mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer principles round 1: 0 🔴, 2 🟡 (0 misread); writtenText alone tracks its prefixes, the walk's bound comment says what the check does now, markdown tested at the bound
This commit is contained in:
@@ -66,7 +66,6 @@ const emailNameSource = "[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+"
|
||||
const emailLabelSource = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
|
||||
const emailAutolink = new RegExp(`<${emailNameSource}@${emailLabelSource}(?:\\.${emailLabelSource})*>`, 'y')
|
||||
const orderedListOpener = /^(\d{1,9})([.)])(?:[ \t]|$)/
|
||||
const ruleCharacters = /^[-*_ \t]*$/
|
||||
const setextUnderline = /^(=+|-+)[ \t]*$/
|
||||
const unicodeWhitespace = /[\t\n\f\r \p{Zs}]/u
|
||||
|
||||
@@ -206,7 +205,11 @@ export function holdsThematicBreak(tail: ThematicBreakTail | undefined, text: st
|
||||
|
||||
// True of every line isThematicBreak takes, and kept for a prefixed line from its parts without reading it.
|
||||
export function holdsOnlyRuleCharacters(text: string): boolean {
|
||||
return ruleCharacters.test(text)
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const character = text.charAt(index)
|
||||
if (!breakMarkers.includes(character) && !spaceOrTab(character)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function isUnicodeWhitespace(character: string): boolean {
|
||||
|
||||
@@ -67,6 +67,22 @@ test('refuses a quote whose prefixed lines would pass V8\'s longest string', ()
|
||||
assert.equal(pastTheBound(adfToLosslessMarkdown(document({ content: [code, code], type: 'blockquote' }))), true)
|
||||
})
|
||||
|
||||
// A code block spelled in 55 characters around its text; the blocks between them take a blank line, and the markdown a closing line break.
|
||||
function codeBlockOf(textLength: number): AdfNode {
|
||||
return { attrs: {}, content: [{ text: `${'a'.repeat(textLength - 1)}\n`, type: 'text' }], type: 'codeBlock' }
|
||||
}
|
||||
|
||||
test('refuses markdown one character past V8\'s longest string', () => {
|
||||
const code = codeBlockOf(268_435_390)
|
||||
assert.equal(pastTheBound(adfToLosslessMarkdown(document(code, code))), true)
|
||||
})
|
||||
|
||||
test('writes markdown exactly as long as V8\'s longest string', () => {
|
||||
const code = codeBlockOf(178_956_908)
|
||||
const written = adfToLosslessMarkdown(document(code, code, code))
|
||||
assert.equal(written.ok && written.value.length, 536870888)
|
||||
})
|
||||
|
||||
test('refuses text past V8\'s longest string', () => {
|
||||
const long = 'a'.repeat(2 ** 28)
|
||||
const result = adfToLosslessMarkdown(document(paragraph({ text: long, type: 'text' }, { text: long, type: 'text' }, { text: long, type: 'text' })))
|
||||
|
||||
@@ -72,7 +72,7 @@ export function walkBlocks(nodes: readonly AdfNode[], path: ConvertErrorPath, de
|
||||
for (const [index, node] of nodes.entries()) {
|
||||
const block = emitBlock(node, [...path, 'content', index], depth, writing)
|
||||
if (!block.ok) return block
|
||||
// Refused here, so an engine holding longer strings stops at the block where V8 throws.
|
||||
// Refused on the measure, before any text is written, so every engine stops at the first block past the bound.
|
||||
const fits = withinLongest(block.value.text.length, block.value)
|
||||
if (!fits.ok) return fits
|
||||
headroom = Math.min(headroom, block.value.headroom)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { holdsOnlyRuleCharacters, isBlankLine } from '../commonmark/grammar.ts'
|
||||
|
||||
// A block's markdown, its prefixes applied only when written out, so a nesting level costs its own blocks rather than the text below it.
|
||||
export type BlockText = Lines & Shape
|
||||
export type BlockText = Readonly<Lines & Shape>
|
||||
type Lines =
|
||||
| { body: BlockText; head: string; indent: string; kind: 'prefixed' }
|
||||
| { body: BlockText; kind: 'quoted' }
|
||||
| { kind: 'joined'; parts: readonly BlockText[] }
|
||||
| { kind: 'text'; lines: readonly string[] }
|
||||
type Prefix = Lines & { kind: 'prefixed' | 'quoted' }
|
||||
// The open prefixes, outermost first, and where the quotes stand among them; a line starts every open prefix, so the unstarted ones are the innermost.
|
||||
type Prefixes = { open: Prefix[]; quotes: number[]; started: number }
|
||||
// The written text's measures; the flags answer for the first line unread, since reading a concatenated string copies it whole, at every level it nests.
|
||||
type Shape = { blankRest: number; emptyRest: number; first: string; firstBlank: boolean; firstCharacter: string; firstRuleCharacters: boolean; length: number; lineCount: number }
|
||||
|
||||
@@ -58,32 +56,35 @@ export function opening(text: BlockText): string {
|
||||
|
||||
export function writtenText(text: BlockText): string {
|
||||
const lines: string[] = []
|
||||
const prefixes: Prefixes = { open: [], quotes: [], started: 0 }
|
||||
// The open prefixes, outermost first, and where the quotes stand among them.
|
||||
const open: Prefix[] = []
|
||||
const quotes: number[] = []
|
||||
// A line starts every open prefix, so the unstarted ones are the innermost.
|
||||
let started = 0
|
||||
const pending: (BlockText | 'leave')[] = [text]
|
||||
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
||||
if (next === 'leave') leavePrefix(prefixes)
|
||||
else if (next.kind === 'text') for (const line of next.lines) lines.push(prefixedLine(line, prefixes))
|
||||
else if (next.kind === 'joined') for (const part of next.parts.slice().reverse()) pending.push(part)
|
||||
if (next === 'leave') {
|
||||
if (open.pop()?.kind === 'quoted') quotes.pop()
|
||||
started = Math.min(started, open.length)
|
||||
} else if (next.kind === 'text') {
|
||||
for (const line of next.lines) {
|
||||
lines.push(prefixedLine(line, open, quotes.at(-1), started))
|
||||
started = open.length
|
||||
}
|
||||
} else if (next.kind === 'joined') for (const part of next.parts.slice().reverse()) pending.push(part)
|
||||
else {
|
||||
if (next.kind === 'quoted') prefixes.quotes.push(prefixes.open.length)
|
||||
prefixes.open.push(next)
|
||||
if (next.kind === 'quoted') quotes.push(open.length)
|
||||
open.push(next)
|
||||
pending.push('leave', next.body)
|
||||
}
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function leavePrefix(prefixes: Prefixes): void {
|
||||
if (prefixes.open.pop()?.kind === 'quoted') prefixes.quotes.pop()
|
||||
prefixes.started = Math.min(prefixes.started, prefixes.open.length)
|
||||
}
|
||||
|
||||
// Innermost first: whether a line is empty when an outer prefix meets it depends on the prefixes inside.
|
||||
function prefixedLine(line: string, prefixes: Prefixes): string {
|
||||
const { open, quotes, started } = prefixes
|
||||
prefixes.started = open.length
|
||||
function prefixedLine(line: string, open: readonly Prefix[], innermostQuote: number | undefined, started: number): string {
|
||||
// Started prefixes inside the innermost quote leave an empty line empty, so its walk begins at that quote.
|
||||
const walked = line === '' && started === open.length ? open.slice(0, (quotes.at(-1) ?? -1) + 1) : open
|
||||
const walked = line === '' && started === open.length ? open.slice(0, (innermostQuote ?? -1) + 1) : open
|
||||
return walked.reduceRight((written, prefix, depth) => {
|
||||
if (prefix.kind === 'quoted') return written === '' ? '>' : `> ${written}`
|
||||
if (depth >= started) return `${prefix.head}${written}`
|
||||
|
||||
Reference in New Issue
Block a user