3
0
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:
2026-10-29 14:07:44 +01:00
committed by Lillem4n
parent 28ae354315
commit 1ff3ad6e9b
4 changed files with 41 additions and 21 deletions
+5 -2
View File
@@ -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 {
+16
View File
@@ -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' })))
+1 -1
View File
@@ -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)
+19 -18
View File
@@ -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}`