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
31 changes: 30 additions & 1 deletion src/__tests__/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,36 @@ describe('Parser', () => {
});

it('removes extracted properties from content', () => {
expect(parser.parse('name: a\n1')[0].content).toEqual(['\n1']);
expect(parser.parse('name: a\n1')[0].content).toEqual(['1']);
});

it('does not consume a word:value line that appears after other content (issue #4)', () => {
// "Example: this line disappears" appears after real content — must stay as content.
const slide = parser.parse('# Heading\n\nExample: this line disappears\n\nMore content.')[0];
expect(slide.properties['Example']).toBeUndefined();
expect(slide.content.join('')).toContain('Example: this line disappears');
});

it('does not consume a word:value line mixed in with regular content', () => {
const slide = parser.parse('Some intro\n\nNote: this should stay\n\nMore text')[0];
expect(slide.properties['Note']).toBeUndefined();
expect(slide.content.join('')).toContain('Note: this should stay');
});

it('stops extracting properties at the first non-property line', () => {
// "name" is a valid property but the next line is not, so class:b further down stays as content.
const slide = parser.parse('name: a\nNot valid prop\nclass: b')[0];
expect(slide.properties.name).toBe('a');
expect(slide.properties.class).toBeUndefined();
expect(slide.content.join('')).toContain('class: b');
});

it('stops extracting properties at a blank line separating front-matter from content', () => {
const slide = parser.parse('name: a\n\nclass: b')[0];
expect(slide.properties.name).toBe('a');
// class: b is after a blank line — it is content, not a property
expect(slide.properties.class).toBeUndefined();
expect(slide.content.join('')).toContain('class: b');
});
});

Expand Down
39 changes: 30 additions & 9 deletions src/mdeck/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,39 @@ function appendTo(element: ParsedSlide | ContentClass, content: ContentItem): vo
}

function extractProperties(source: string, properties: Record<string, string>): string {
const propertyFinder = /^\n*([-\w]+):([^$\n]*)|\n*(?:<!--\s*)([-\w]+):([^$\n]*?)(?:\s*-->)/i;
if (typeof source !== 'string') return source as unknown as string;

// Extract inline HTML comment properties anywhere in the source (they are invisible markers).
const commentFinder = /\n*(?:<!--\s*)([-\w]+):([^$\n]*?)(?:\s*-->)/gi;
let match: RegExpExecArray | null;
while ((match = propertyFinder.exec(source)) !== null) {
while ((match = commentFinder.exec(source)) !== null) {
source = source.slice(0, match.index) + source.slice(match.index + match[0].length);
if (match[1] !== undefined) {
properties[match[1].trim()] = match[2].trim();
} else {
properties[match[3].trim()] = match[4].trim();
}
propertyFinder.lastIndex = match.index;
properties[match[1].trim()] = match[2].trim();
commentFinder.lastIndex = match.index;
}

// Extract plain `key: value` properties only from the leading front-matter block.
// Stop as soon as a line that is not a property (or blank) is encountered so that
// content lines like "Example: this disappears" are never consumed as properties.
const lines = source.split('\n');
const consumed: number[] = [];
const propertyLine = /^([-\w]+):([^$\n]*)$/i;
let i = 0;
// Skip leading blank lines.
while (i < lines.length && lines[i].trim() === '') { i++; }
// Consume contiguous property lines.
while (i < lines.length) {
const line = lines[i];
if (line.trim() === '') break; // blank line ends the front-matter block
const m = propertyLine.exec(line);
if (!m) break; // non-property line ends the block
properties[m[1].trim()] = m[2].trim();
consumed.push(i);
i++;
}
return source;
// Remove consumed lines from source.
const remaining = lines.filter((_, idx) => !consumed.includes(idx));
return remaining.join('\n');
}

function cleanInput(source: string): string {
Expand Down