Skip to content

Commit 09b907d

Browse files
committed
stuff
1 parent 93a9e92 commit 09b907d

File tree

2 files changed

+128
-55
lines changed

2 files changed

+128
-55
lines changed

packages/sv-utils/src/tests/md.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,29 +68,31 @@ describe('md upsert', () => {
6868
expect(result).toBe('existing content\nnew line\n');
6969
});
7070

71-
it('should append by default', () => {
72-
const content = '# Section\n\nold content\n\n## Next\n';
73-
const withDefault = upsert(content, ['new'], { header: '# Section' });
74-
const withExplicit = upsert(content, ['new'], { header: '# Section', mode: 'append' });
75-
expect(withDefault).toBe('# Section\n\nold content\nnew\n\n## Next\n');
76-
expect(withDefault).toBe(withExplicit);
71+
it('works with object header syntax', () => {
72+
const content = '# Section\n\nexisting';
73+
const result = upsert(content, ['new'], { header: { name: '# Section' } });
74+
expect(result).toBe('# Section\n\nexisting\nnew\n');
7775
});
7876

79-
it('adds content right after header', () => {
80-
const content = '# Hello\n\nSome content\n\n## World\n';
81-
const result = upsert(content, ['new line'], { header: '# Hello', mode: 'prepend' });
82-
expect(result).toBe('# Hello\n\nnew line\nSome content\n\n## World\n');
77+
it('adds child header under parent section', () => {
78+
const content = '# Parent\n\nparent content';
79+
const result = upsert(content, ['child content'], {
80+
header: { name: '## Child', parent: '# Parent' }
81+
});
82+
expect(result).toBe('# Parent\n\nparent content\n\n## Child\n\nchild content\n');
8383
});
8484

85-
it('prepend multiple lines', () => {
86-
const content = '# Section\n\nexisting content';
87-
const result = upsert(content, ['line 1', 'line 2'], { header: '# Section', mode: 'prepend' });
88-
expect(result).toBe('# Section\n\nline 1\nline 2\nexisting content\n');
85+
it('creates parent section if not found', () => {
86+
const content = '# Existing';
87+
const result = upsert(content, ['child content'], {
88+
header: { name: '## Child', parent: '# New Parent' }
89+
});
90+
expect(result).toBe('# Existing\n\n# New Parent\n\n## Child\n\nchild content\n');
8991
});
9092

91-
it('prepend works with different header levels', () => {
92-
const content = '## H2\n\nexisting';
93-
const result = upsert(content, ['new'], { header: '## H2', mode: 'prepend' });
94-
expect(result).toBe('## H2\n\nnew\nexisting\n');
93+
it('works with nested headers', () => {
94+
const content = '# Parent\n\n## Child1\n\ncontent1\n\n## Child2\n\ncontent2';
95+
const result = upsert(content, ['new'], { header: { name: '## Child1' } });
96+
expect(result).toBe('# Parent\n\n## Child1\n\ncontent1\nnew\n\n## Child2\n\ncontent2\n');
9597
});
9698
});

packages/sv-utils/src/tooling/md.ts

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,8 @@ type Header = `${'#' | '##' | '###' | '####' | '#####' | '######'} ${string}`;
22

33
const HEADER_REGEX = /^#{1,6} .+$/m;
44

5-
export function upsert(
6-
content: string,
7-
lines: Array<string | false | undefined | null | 0 | 0n>,
8-
options?: {
9-
header?: Header;
10-
mode?: 'prepend' | 'append';
11-
}
12-
): string {
13-
const { header, mode = 'append' } = options ?? {};
14-
const linesToAdd = lines.filter(Boolean).join('\n');
15-
16-
if (!header) {
17-
return joinContent(content, linesToAdd);
18-
}
19-
20-
const headerMatch = findHeader(content, header);
21-
22-
if (!headerMatch) {
23-
return joinContent(content, header, linesToAdd);
24-
}
25-
const { end: headerLineEnd, after: restOfContent } = headerMatch;
26-
27-
const nextHeaderMatch = restOfContent.match(HEADER_REGEX);
28-
29-
let insertPos: number;
30-
if (mode === 'prepend') {
31-
insertPos = headerLineEnd + 1;
32-
} else if (nextHeaderMatch) {
33-
insertPos = headerLineEnd + 1 + restOfContent.indexOf(nextHeaderMatch[0]);
34-
} else {
35-
insertPos = content.length;
36-
}
37-
38-
const before = content.slice(0, insertPos);
39-
const after = content.slice(insertPos);
40-
41-
return joinContent(before, linesToAdd, after);
5+
function getHeaderLevel(header: Header): number {
6+
return header.split(' ')[0].length;
427
}
438

449
function escapeRegex(str: string): string {
@@ -92,3 +57,109 @@ function findHeader(
9257
after: content.slice(end + 1)
9358
};
9459
}
60+
61+
function findSection(
62+
content: string,
63+
header: Header
64+
): {
65+
before: string;
66+
after: string;
67+
header: Header;
68+
innerContent: string;
69+
start: number;
70+
end: number;
71+
} | null {
72+
const headerMatch = findHeader(content, header);
73+
74+
if (!headerMatch) return null;
75+
76+
const { start, end: headerEnd, before } = headerMatch;
77+
const level = getHeaderLevel(header);
78+
const nextHeaderRegex = new RegExp(`^#{${level}} `, 'm');
79+
const afterHeader = content.slice(headerEnd + 1);
80+
const nextHeaderMatch = afterHeader.match(nextHeaderRegex);
81+
82+
let end: number;
83+
if (nextHeaderMatch) {
84+
end = headerEnd + 1 + afterHeader.indexOf(nextHeaderMatch[0]);
85+
} else {
86+
end = content.length;
87+
}
88+
89+
const innerContent = content.slice(headerEnd + 1, end);
90+
const after = content.slice(end);
91+
92+
return {
93+
before,
94+
after,
95+
header,
96+
innerContent,
97+
start,
98+
end
99+
};
100+
}
101+
102+
export function upsert(
103+
content: string,
104+
lines: Array<string | false | undefined | null | 0 | 0n>,
105+
options?: {
106+
header?:
107+
| Header
108+
| {
109+
name: Header;
110+
parent?: Header;
111+
};
112+
}
113+
): string {
114+
const { header } = options ?? {};
115+
const linesToAdd = lines.filter(Boolean).join('\n');
116+
117+
if (!header) {
118+
return joinContent(content, linesToAdd);
119+
}
120+
121+
if (typeof header === 'string') {
122+
return asdf(content, linesToAdd, header);
123+
} else {
124+
if (!header.parent) {
125+
return asdf(content, linesToAdd, header.name);
126+
}
127+
const section = findSection(content, header.parent);
128+
129+
if (!section) {
130+
return joinContent(content, header.parent, header.name, linesToAdd);
131+
}
132+
133+
return joinContent(
134+
section.before,
135+
section.header,
136+
section.innerContent,
137+
header.name,
138+
linesToAdd,
139+
section.after
140+
);
141+
}
142+
}
143+
144+
function asdf(content: string, linesToAdd: string, header: Header): string {
145+
const section = findSection(content, header);
146+
147+
if (!section) {
148+
return joinContent(content, header, linesToAdd);
149+
}
150+
151+
const { start, end, innerContent } = section;
152+
const firstNextHeaderMatch = innerContent.match(HEADER_REGEX);
153+
154+
let insertPos: number;
155+
if (firstNextHeaderMatch) {
156+
insertPos = start + header.length + 1 + innerContent.indexOf(firstNextHeaderMatch[0]);
157+
} else {
158+
insertPos = end;
159+
}
160+
161+
const before = content.slice(0, insertPos);
162+
const after = content.slice(insertPos);
163+
164+
return joinContent(before, linesToAdd, after);
165+
}

0 commit comments

Comments
 (0)