mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-12 16:08:59 +02:00
fix(design-md): text-level edits keep CRLF, one section-boundary rule, control characters quoted
- insertMarker and spliceSection normalized every line ending to LF, so a CRLF DESIGN.md came back rewritten beyond the one line they promised to touch. Both detect the file's dominant line ending and restore it. - parseDesignMd and spliceSection each walked headings with their own fence tracking; they now share headingLines (and upsertSection shares headingMatches). An unclosed ``` is treated as prose for that file: it used to swallow every later section on a splice. - A token value carrying a control character (an LLM-extracted font family with an embedded newline) was emitted as a bare multi-line scalar that Bun.YAML rejects, turning a freshly written DESIGN.md into frontmatter-unparsable; needsQuotes routes it through the quoted form. - The marker-line regex variants are built once beside YAML_MARKER_RE; the dead setMarker export and a no-op ternary are gone; LEGACY_HEADINGS derives from the identity list; the header diagram names the text-level editors as the write path for user-owned files; the bin validates and prints the mark choices from FORMAT_CHOICES. Tests: CRLF round-trips for both editors, a fenced ## inside a section and an unclosed fence, and a newline-bearing scalar parsing back. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b2e67d0097
commit
ae5a5298e0
+33
-3
@@ -15,7 +15,7 @@ import * as path from 'path';
|
||||
import { spawnSync } from 'child_process';
|
||||
import {
|
||||
parseDesignMd, detectFormat, renderDesignMd, upsertSection, convertLegacy, tokensFlat,
|
||||
emitYamlBlock, setMarker, specSkeleton, spliceSection, insertMarker, CANONICAL_SECTIONS, TOKEN_GROUPS, isLegacyGstackFormat,
|
||||
emitYamlBlock, specSkeleton, spliceSection, insertMarker, CANONICAL_SECTIONS, TOKEN_GROUPS, isLegacyGstackFormat,
|
||||
} from '../lib/design-md';
|
||||
import { updateDesignMd, readDesignConstraints } from '../design/src/memory';
|
||||
|
||||
@@ -196,9 +196,9 @@ describe('render + upsert', () => {
|
||||
expect(insertMarker(kept, 'legacy-keep')).toBe(kept);
|
||||
});
|
||||
|
||||
test('setMarker on a spec file writes the YAML comment on line 2 and nothing else moves', () => {
|
||||
test('a marker on the parsed doc renders as the YAML comment on line 2 and nothing else moves', () => {
|
||||
const noMarker = SPEC.replace('# gstack: design-md-format=spec\n', '');
|
||||
const out = renderDesignMd(setMarker(parseDesignMd(noMarker), 'spec'));
|
||||
const out = renderDesignMd({ ...parseDesignMd(noMarker), marker: 'spec' });
|
||||
expect(out.split('\n').slice(0, 3)).toEqual(['---', '# gstack: design-md-format=spec', 'name: Heritage']);
|
||||
});
|
||||
});
|
||||
@@ -545,3 +545,33 @@ describe('design binary: updateDesignMd is frontmatter-safe', () => {
|
||||
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
});
|
||||
|
||||
describe('text-level editors keep line endings and respect fences', () => {
|
||||
const SPEC_LF = ['---', 'name: x', 'colors:', ' a: "#fff"', '---', '', '## Overview', '', 'o', '', '## Colors', '', 'c', ''].join('\n');
|
||||
|
||||
test('insertMarker and spliceSection preserve CRLF line endings', () => {
|
||||
const crlf = SPEC_LF.replace(/\n/g, '\r\n');
|
||||
const marked = insertMarker(crlf, 'spec');
|
||||
expect(marked).toBe(crlf.replace('---\r\n', '---\r\n# gstack: design-md-format=spec\r\n'));
|
||||
expect(marked).not.toMatch(/[^\r]\n/);
|
||||
const legacyCrlf = '# T\r\n\r\n## Product Context\r\n\r\np\r\n';
|
||||
expect(insertMarker(legacyCrlf, 'legacy-keep')).toBe('<!-- gstack: design-md-format=legacy-keep -->\r\n' + legacyCrlf);
|
||||
const spliced = spliceSection(crlf, 'Colors', 'Ink only.');
|
||||
expect(spliced).toBe(SPEC_LF.replace('## Colors\n\nc\n', '## Colors\n\nInk only.\n').replace(/\n/g, '\r\n'));
|
||||
expect(spliceSection(SPEC_LF, 'Colors', 'Ink only.')).not.toContain('\r');
|
||||
});
|
||||
|
||||
test('a fenced ## inside a section does not end it; an unclosed fence is prose, so later sections survive', () => {
|
||||
const src = '## A\n\nbody\n\n```md\n## Not a heading\n```\n\n## B\n\nb body\n';
|
||||
expect(spliceSection(src, 'A', 'x')).toBe('## A\n\nx\n\n## B\n\nb body\n');
|
||||
expect(parseDesignMd(src).sections.map(s => s.heading)).toEqual(['A', 'B']);
|
||||
const unclosed = '## A\n\nbody\n\n```\nunclosed\n\n## B\n\nb body\n';
|
||||
expect(spliceSection(unclosed, 'A', 'x')).toBe('## A\n\nx\n\n## B\n\nb body\n');
|
||||
expect(parseDesignMd(unclosed).sections.map(s => s.heading)).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
test('a token value with an embedded newline is quoted and parses back', () => {
|
||||
const yaml = emitYamlBlock({ typography: { body: { fontFamily: 'Foo\nBar', fontSize: '16px\tx' } } });
|
||||
expect(Bun.YAML.parse(yaml)).toEqual({ typography: { body: { fontFamily: 'Foo\nBar', fontSize: '16px\tx' } } });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user