Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions __tests__/source-map/code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ describe('parseMdWithSourceMap: code.value → raw source', () => {
expect(sourceMap.getSourceRange(node, 0, 0).start.offset).toBe(4);
});

test('preserves the empty range before extra closing-fence indentation', () => {
const md = '```\n ```';
const { ast, sourceMap } = parseMdWithSourceMap(md);
const node = nodesOfType(ast, 'code')[0];
expect(node.value).toBe('');

const range = sourceMap.getSourceRange(node, 0, 0);
expect(range.start.offset).toBe(4);
expect(range.end.offset).toBe(4);
});

test.each([
['```', 3],
['~~~', 3],
Expand Down
13 changes: 10 additions & 3 deletions scripts/bench-parse.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ const SMOKE_GROWTH_CHECKS = [
// Child mode and parity mode
// ---------------------------------------------------------------------------

function makeState() {
function makeState(source) {
return {
source,
segments: new WeakMap(),
inlineCodeSegments: new WeakMap(),
codeSegments: new WeakMap(),
Expand Down Expand Up @@ -188,7 +189,10 @@ function phaseRunner(bundle, phase) {
case 'B2':
return (md) => fromMarkdown(md, {
extensions: micromarkExtensions,
mdastExtensions: [...fromMarkdownExtensions, recordingExtension(makeState())],
mdastExtensions: [
...fromMarkdownExtensions,
recordingExtension(makeState(md)),
],
});
case 'C':
return (md) => parseMdWithSourceMap(md).ast;
Expand Down Expand Up @@ -264,7 +268,10 @@ async function runParity() {
}));
const b2 = canon(fromMarkdown(md, {
extensions: micromarkExtensions,
mdastExtensions: [...fromMarkdownExtensions, recordingExtension(makeState())],
mdastExtensions: [
...fromMarkdownExtensions,
recordingExtension(makeState(md)),
],
}));
const c = canon(parseMdWithSourceMap(md).ast);
return { bytes: Buffer.byteLength(md), aEqC: a === c, bEqC: b === c, b2EqC: b2 === c };
Expand Down
6 changes: 5 additions & 1 deletion src/source-map/build-source-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ import type {
const { micromarkExtensions, fromMarkdownExtensions } = getParserExtensions();

interface RecordingState {
/** Markdown source used for parser-confirmed indentation spans. */
source: string
/** node -> ordered, gap-free, non-overlapping segments. */
segments: WeakMap<object, MarkdownSourceMapSegment[]>
/** inlineCode node -> parser-recorded value segments. */
inlineCodeSegments: WeakMap<object, MarkdownSourceMapSegment[]>
/** code node -> value segments (see buildCodeSegments). */
/** code node -> parser-recorded or indented-code value segments. */
codeSegments: WeakMap<object, MarkdownSourceMapSegment[]>
/** code node -> source point for an empty value. */
emptyCodeOffsets: WeakMap<object, number>
Expand Down Expand Up @@ -437,6 +439,7 @@ function resolveSegmentRange(options: SegmentRangeOptions): ParsedPosition {
*/
export const parseMdWithSourceMap = (md: string): ParsedMarkdownDocument => {
const state: RecordingState = {
source: md,
segments: new WeakMap(),
inlineCodeSegments: new WeakMap(),
codeSegments: new WeakMap(),
Expand Down Expand Up @@ -471,6 +474,7 @@ export const parseMdWithSourceMap = (md: string): ParsedMarkdownDocument => {
if (
node.type === 'code'
&& hasStringField(node, 'value')
&& !state.codeSegments.has(node)
) {
const mapping = buildCodeSegments(md, node);
if (mapping) {
Expand Down
152 changes: 1 addition & 151 deletions src/source-map/code-segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,67 +68,6 @@ function skipBlockQuoteMarkers(
return offset;
}

function skipIndentation(
md: string,
start: number,
end: number,
columns: number,
): number {
let offset = start;
let removed = 0;
while (offset < end && removed < columns) {
const char = md.charCodeAt(offset);
if (char === 32) {
offset++;
removed++;
}
else if (char === 9) {
offset++;
removed += 4 - (removed % 4);
}
else {
break;
}
}
return offset;
}

function fencedIndentation(
md: string,
lineStartOffset: number,
fenceStart: number,
quoteDepth: number,
): number {
let offset = lineStartOffset;
if (quoteDepth > 0) {
const afterMarkers = skipBlockQuoteMarkers(
md,
lineStartOffset,
fenceStart,
quoteDepth,
);
if (afterMarkers === undefined)
return -1;
offset = afterMarkers;
}
let indentation = 0;
while (offset < fenceStart) {
const char = md.charCodeAt(offset);
if (char === 32) {
offset++;
indentation++;
}
else if (char === 9) {
offset++;
indentation += 4 - (indentation % 4);
}
else {
return -1;
}
}
return indentation;
}

function trimTrailingLineEnding(md: string, spans: SourceSpan[]): void {
const last = spans[spans.length - 1];
if (!last)
Expand Down Expand Up @@ -189,88 +128,6 @@ function segmentsFromSpans(
return valueOffset === value.length ? segments : undefined;
}

function buildFencedCodeSegments(
md: string,
node: { value: string; position?: ParsedPosition },
): CodeSegments | undefined {
const position = node.position;
if (!position)
return undefined;
const start = position.start.offset;
const end = position.end.offset;
const marker = md.charCodeAt(start);
if (marker !== 96 && marker !== 126)
return undefined;

let fenceLength = 0;
while (md.charCodeAt(start + fenceLength) === marker) fenceLength++;
if (fenceLength < 3)
return undefined;

const physicalLineStart = lineStart(md, 0, start);
const quoteDepth = blockQuoteDepth(md, physicalLineStart, start);
const openingIndent = fencedIndentation(md, physicalLineStart, start, quoteDepth);
if (openingIndent < 0)
return undefined;
// A blockquote code node can end before its final physical line ending, so
// derive the opening line boundary from the complete Markdown rather than
// the node's position span. This keeps an unclosed empty fence's insertion
// point at the actual EOF.
const openingLineEnd = lineEnd(md, start, md.length);
let contentEnd = end;
let hasClosingFence = false;
const closingLineStart = lineStart(md, start, end);
let closingFenceStart = openingLineEnd;
if (closingLineStart >= openingLineEnd && closingLineStart < end) {
const closingStart = quoteDepth === 0
? closingLineStart
: skipBlockQuoteMarkers(md, closingLineStart, end, quoteDepth);
if (closingStart === undefined)
return undefined;
closingFenceStart = closingStart;
closingFenceStart = skipIndentation(
md,
closingFenceStart,
end,
openingIndent,
);
const closing = md.slice(closingFenceStart, end);
const closingMatch = /^( {0,3})(`+|~+)[ \t]*$/.exec(closing);
if (
closingMatch
&& closingMatch[2].charCodeAt(0) === marker
&& closingMatch[2].length >= fenceLength
) {
contentEnd = closingLineStart;
hasClosingFence = true;
}
}

const spans: SourceSpan[] = [];
let offset = openingLineEnd;
while (offset < contentEnd) {
const endOfLine = lineEnd(md, offset, contentEnd);
let contentStart = quoteDepth === 0
? offset
: skipBlockQuoteMarkers(md, offset, endOfLine, quoteDepth);
if (contentStart === undefined)
return undefined;
contentStart = skipIndentation(md, contentStart, endOfLine, openingIndent);
spans.push({ start: contentStart, end: endOfLine });
offset = endOfLine;
}
const emptyOffset = spans[0]?.start
?? (hasClosingFence ? closingFenceStart : openingLineEnd);
trimTrailingLineEnding(md, spans);
const segments = segmentsFromSpans(md, spans, node.value);
if (!segments)
return undefined;
return {
segments,
emptyOffset,
};
}

function buildIndentedCodeSegmentsFromIndentation(
md: string,
node: { value: string; position?: ParsedPosition },
Expand Down Expand Up @@ -422,12 +279,5 @@ export function buildCodeSegments(
md: string,
node: { value: string; position?: ParsedPosition },
): CodeSegments | undefined {
const position = node.position;
if (!position)
return undefined;
const start = position.start.offset;
const marker = md.charCodeAt(start);
return marker === 96 || marker === 126
? buildFencedCodeSegments(md, node)
: buildIndentedCodeSegments(md, node);
return buildIndentedCodeSegments(md, node);
}
Loading
Loading