@portel/photon 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +29 -8
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js +3 -0
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +167 -48
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +578 -0
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
- package/dist/auto-ui/bridge/renderers.js +7 -3
- package/dist/auto-ui/bridge/renderers.js.map +1 -1
- package/dist/auto-ui/bridge/types.d.ts +6 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -1
- package/dist/auto-ui/frontend/pure-view.html +289 -0
- package/dist/auto-ui/photon-bridge.d.ts +11 -0
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
- package/dist/auto-ui/photon-bridge.js +75 -1
- package/dist/auto-ui/photon-bridge.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +29 -3
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/beam-form.bundle.js +5707 -0
- package/dist/beam-form.bundle.js.map +7 -0
- package/dist/beam.bundle.js +1947 -523
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +15 -2
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/daemon/client.d.ts +5 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +50 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +15 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +142 -11
- package/dist/daemon/manager.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +10 -2
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +50 -3
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +9 -0
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +115 -42
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/meta.d.ts +51 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +320 -0
- package/dist/meta.js.map +1 -0
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +30 -5
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +1 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +33 -21
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/docs/ui/docs.html +441 -0
- package/dist/photons/docs.photon.d.ts +237 -0
- package/dist/photons/docs.photon.d.ts.map +1 -0
- package/dist/photons/docs.photon.js +483 -0
- package/dist/photons/docs.photon.js.map +1 -0
- package/dist/photons/docs.photon.ts +536 -0
- package/dist/photons/slides.photon.d.ts +212 -0
- package/dist/photons/slides.photon.d.ts.map +1 -0
- package/dist/photons/slides.photon.js +355 -0
- package/dist/photons/slides.photon.js.map +1 -0
- package/dist/photons/slides.photon.ts +370 -0
- package/dist/photons/spreadsheet/ui/spreadsheet.html +779 -0
- package/dist/photons/spreadsheet.photon.d.ts +554 -0
- package/dist/photons/spreadsheet.photon.d.ts.map +1 -0
- package/dist/photons/spreadsheet.photon.js +1050 -0
- package/dist/photons/spreadsheet.photon.js.map +1 -0
- package/dist/photons/spreadsheet.photon.ts +1239 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +17 -57
- package/dist/server.js.map +1 -1
- package/dist/shared/error-handler.d.ts +8 -0
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +50 -0
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared-utils.d.ts +3 -2
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +4 -3
- package/dist/shared-utils.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs — Markdown Document Editor with PDF Export
|
|
3
|
+
*
|
|
4
|
+
* A document editor backed by plain markdown files with YAML frontmatter.
|
|
5
|
+
* Each instance is a document: `_use('quarterly-report')` → `quarterly-report.md`.
|
|
6
|
+
* Pass a full path to open any file: `_use('/path/to/doc.md')`.
|
|
7
|
+
*
|
|
8
|
+
* Features page-aware preview via Paged.js, TOC generation, footnotes,
|
|
9
|
+
* custom containers (note/warning/tip), multi-column layouts, and PDF export.
|
|
10
|
+
*
|
|
11
|
+
* @version 1.0.0
|
|
12
|
+
* @runtime ^1.14.0
|
|
13
|
+
* @tags document, markdown, pdf, writing, authoring
|
|
14
|
+
* @icon 📄
|
|
15
|
+
* @stateful
|
|
16
|
+
* @ui editor ./ui/docs.html
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import * as os from 'os';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_DOC = `---
|
|
24
|
+
title: Untitled Document
|
|
25
|
+
author: ""
|
|
26
|
+
date: ${new Date().toISOString().split('T')[0]}
|
|
27
|
+
size: A4
|
|
28
|
+
margins:
|
|
29
|
+
top: 2.5cm
|
|
30
|
+
bottom: 2.5cm
|
|
31
|
+
left: 3cm
|
|
32
|
+
right: 2cm
|
|
33
|
+
header:
|
|
34
|
+
right: "{date}"
|
|
35
|
+
footer:
|
|
36
|
+
center: "Page {page} of {pages}"
|
|
37
|
+
toc: false
|
|
38
|
+
numbersections: false
|
|
39
|
+
theme: default
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
# Untitled Document
|
|
43
|
+
|
|
44
|
+
Start writing here. This is a plain markdown document with superpowers.
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **YAML frontmatter** controls page layout, headers, footers, and theme
|
|
49
|
+
- **Page breaks** with \`---pagebreak---\`
|
|
50
|
+
- **Footnotes** with \`[^1]\` syntax
|
|
51
|
+
- **Callout boxes** with \`::: note\`, \`::: warning\`, \`::: tip\`
|
|
52
|
+
- **Multi-column layouts** with \`::: columns\`
|
|
53
|
+
- **Table of contents** with \`[[toc]]\`
|
|
54
|
+
- **PDF export** that matches exactly what you see
|
|
55
|
+
|
|
56
|
+
## Next Steps
|
|
57
|
+
|
|
58
|
+
Ask AI to help you write, restructure, or format this document.
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export default class Docs {
|
|
62
|
+
protected settings = {
|
|
63
|
+
/** @property Directory where document files are stored */
|
|
64
|
+
folder: path.join(os.homedir(), 'Documents', 'docs'),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
declare memory: {
|
|
68
|
+
get<T>(key: string): Promise<T | null>;
|
|
69
|
+
set(key: string, value: unknown): Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
declare emit: (payload: { event: string; data: unknown }) => void;
|
|
72
|
+
declare instanceName: string;
|
|
73
|
+
|
|
74
|
+
// ── File Resolution ─────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
private get defaultFolder(): string {
|
|
77
|
+
return this.settings?.folder || path.join(os.homedir(), 'Documents', 'docs');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private get docPath(): string {
|
|
81
|
+
const name = this.instanceName || 'untitled';
|
|
82
|
+
if (path.isAbsolute(name)) return name.endsWith('.md') ? name : name + '.md';
|
|
83
|
+
if (name.includes('/') || name.includes('\\')) {
|
|
84
|
+
const resolved = path.resolve(name);
|
|
85
|
+
return resolved.endsWith('.md') ? resolved : resolved + '.md';
|
|
86
|
+
}
|
|
87
|
+
const dir = this.defaultFolder;
|
|
88
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
89
|
+
return path.join(dir, name.endsWith('.md') ? name : name + '.md');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async onInitialize() {
|
|
93
|
+
const dir = this.defaultFolder;
|
|
94
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
95
|
+
if (!existsSync(this.docPath)) {
|
|
96
|
+
await fs.writeFile(this.docPath, DEFAULT_DOC, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Editor ──────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Open the document editor UI
|
|
104
|
+
* @ui editor
|
|
105
|
+
* @autorun
|
|
106
|
+
*/
|
|
107
|
+
async main() {
|
|
108
|
+
const markdown = await this.readDoc();
|
|
109
|
+
const { frontmatter, body } = parseFrontmatter(markdown);
|
|
110
|
+
const outline = buildOutline(body);
|
|
111
|
+
return {
|
|
112
|
+
file: path.basename(this.docPath),
|
|
113
|
+
markdown,
|
|
114
|
+
frontmatter,
|
|
115
|
+
outline,
|
|
116
|
+
stats: computeStats(body),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read the document markdown
|
|
122
|
+
* @readOnly
|
|
123
|
+
*/
|
|
124
|
+
async read() {
|
|
125
|
+
return { file: path.basename(this.docPath), markdown: await this.readDoc() };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Save the full document markdown
|
|
130
|
+
* @param markdown Full markdown content with YAML frontmatter
|
|
131
|
+
* @ui editor
|
|
132
|
+
*/
|
|
133
|
+
async save({ markdown }: { markdown: string }) {
|
|
134
|
+
await fs.writeFile(this.docPath, markdown, 'utf8');
|
|
135
|
+
const { frontmatter, body } = parseFrontmatter(markdown);
|
|
136
|
+
const result = {
|
|
137
|
+
file: path.basename(this.docPath),
|
|
138
|
+
markdown,
|
|
139
|
+
frontmatter,
|
|
140
|
+
outline: buildOutline(body),
|
|
141
|
+
stats: computeStats(body),
|
|
142
|
+
};
|
|
143
|
+
this.emit({ event: 'docChanged', data: result });
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Structural Editing ──────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get the document's heading structure for navigation
|
|
151
|
+
* @readOnly
|
|
152
|
+
*/
|
|
153
|
+
async outline() {
|
|
154
|
+
const body = getBody(await this.readDoc());
|
|
155
|
+
return { outline: buildOutline(body) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Edit a specific section by heading path
|
|
160
|
+
* @param section Heading text or path like "Chapter 3/Introduction"
|
|
161
|
+
* @param markdown New content for that section (everything under the heading until next same-level heading)
|
|
162
|
+
* @ui editor
|
|
163
|
+
*/
|
|
164
|
+
async edit({ section, markdown: newContent }: { section: string; markdown: string }) {
|
|
165
|
+
const doc = await this.readDoc();
|
|
166
|
+
const { frontmatterRaw, body } = splitDoc(doc);
|
|
167
|
+
const updated = replaceSection(body, section, newContent);
|
|
168
|
+
if (updated === null) return { error: `Section "${section}" not found` };
|
|
169
|
+
const full = frontmatterRaw + updated;
|
|
170
|
+
await fs.writeFile(this.docPath, full, 'utf8');
|
|
171
|
+
const { frontmatter } = parseFrontmatter(full);
|
|
172
|
+
const result = {
|
|
173
|
+
file: path.basename(this.docPath),
|
|
174
|
+
markdown: full,
|
|
175
|
+
frontmatter,
|
|
176
|
+
outline: buildOutline(updated),
|
|
177
|
+
stats: computeStats(updated),
|
|
178
|
+
};
|
|
179
|
+
this.emit({ event: 'docChanged', data: result });
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Append content at the end of the document or after a specific section
|
|
185
|
+
* @param markdown Content to append
|
|
186
|
+
* @param after Optional heading text — inserts after that section instead of end
|
|
187
|
+
* @ui editor
|
|
188
|
+
*/
|
|
189
|
+
async append({ markdown: content, after }: { markdown: string; after?: string }) {
|
|
190
|
+
const doc = await this.readDoc();
|
|
191
|
+
const { frontmatterRaw, body } = splitDoc(doc);
|
|
192
|
+
let updated: string;
|
|
193
|
+
if (after) {
|
|
194
|
+
updated = insertAfterSection(body, after, content);
|
|
195
|
+
} else {
|
|
196
|
+
updated = body.trimEnd() + '\n\n' + content + '\n';
|
|
197
|
+
}
|
|
198
|
+
const full = frontmatterRaw + updated;
|
|
199
|
+
await fs.writeFile(this.docPath, full, 'utf8');
|
|
200
|
+
const { frontmatter } = parseFrontmatter(full);
|
|
201
|
+
const result = {
|
|
202
|
+
file: path.basename(this.docPath),
|
|
203
|
+
markdown: full,
|
|
204
|
+
frontmatter,
|
|
205
|
+
outline: buildOutline(updated),
|
|
206
|
+
stats: computeStats(updated),
|
|
207
|
+
};
|
|
208
|
+
this.emit({ event: 'docChanged', data: result });
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Search & Replace ────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Find text in the document with optional fuzzy matching
|
|
216
|
+
* @param query Search text
|
|
217
|
+
* @param fuzzy Enable fuzzy matching {@default false}
|
|
218
|
+
* @param scope Limit search to a section heading
|
|
219
|
+
* @readOnly
|
|
220
|
+
*/
|
|
221
|
+
async find({ query, fuzzy, scope }: { query: string; fuzzy?: boolean; scope?: string }) {
|
|
222
|
+
const body = getBody(await this.readDoc());
|
|
223
|
+
const searchIn = scope ? extractSection(body, scope) || body : body;
|
|
224
|
+
const lines = searchIn.split('\n');
|
|
225
|
+
const queryLower = query.toLowerCase();
|
|
226
|
+
|
|
227
|
+
const matches: { line: number; text: string; context: string }[] = [];
|
|
228
|
+
for (let i = 0; i < lines.length; i++) {
|
|
229
|
+
const lineLower = lines[i].toLowerCase();
|
|
230
|
+
const found = fuzzy ? fuzzyMatch(lineLower, queryLower) : lineLower.includes(queryLower);
|
|
231
|
+
if (found) {
|
|
232
|
+
matches.push({
|
|
233
|
+
line: i + 1,
|
|
234
|
+
text: lines[i],
|
|
235
|
+
context: lines.slice(Math.max(0, i - 1), i + 2).join('\n'),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { query, fuzzy: !!fuzzy, scope: scope || null, matches, total: matches.length };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Find and replace text in the document
|
|
244
|
+
* @param pattern Text to find (string or regex pattern)
|
|
245
|
+
* @param replacement Replacement text
|
|
246
|
+
* @param scope Limit to a section heading
|
|
247
|
+
* @param all Replace all occurrences {@default true}
|
|
248
|
+
* @ui editor
|
|
249
|
+
*/
|
|
250
|
+
async replace({
|
|
251
|
+
pattern,
|
|
252
|
+
replacement,
|
|
253
|
+
scope,
|
|
254
|
+
all,
|
|
255
|
+
}: {
|
|
256
|
+
pattern: string;
|
|
257
|
+
replacement: string;
|
|
258
|
+
scope?: string;
|
|
259
|
+
all?: boolean;
|
|
260
|
+
}) {
|
|
261
|
+
const doc = await this.readDoc();
|
|
262
|
+
const { frontmatterRaw, body } = splitDoc(doc);
|
|
263
|
+
|
|
264
|
+
let target = scope ? extractSection(body, scope) || body : body;
|
|
265
|
+
const replaceAll = all !== false;
|
|
266
|
+
|
|
267
|
+
let count = 0;
|
|
268
|
+
if (replaceAll) {
|
|
269
|
+
const parts = target.split(pattern);
|
|
270
|
+
count = parts.length - 1;
|
|
271
|
+
target = parts.join(replacement);
|
|
272
|
+
} else {
|
|
273
|
+
if (target.includes(pattern)) {
|
|
274
|
+
target = target.replace(pattern, replacement);
|
|
275
|
+
count = 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const updated = scope ? body.replace(extractSection(body, scope) || '', target) : target;
|
|
280
|
+
const full = frontmatterRaw + updated;
|
|
281
|
+
await fs.writeFile(this.docPath, full, 'utf8');
|
|
282
|
+
const { frontmatter } = parseFrontmatter(full);
|
|
283
|
+
const result = {
|
|
284
|
+
file: path.basename(this.docPath),
|
|
285
|
+
markdown: full,
|
|
286
|
+
frontmatter,
|
|
287
|
+
outline: buildOutline(updated),
|
|
288
|
+
stats: computeStats(updated),
|
|
289
|
+
replacements: count,
|
|
290
|
+
};
|
|
291
|
+
this.emit({ event: 'docChanged', data: result });
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Document Management ─────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* List saved documents in the docs folder
|
|
299
|
+
* @readOnly
|
|
300
|
+
*/
|
|
301
|
+
async list() {
|
|
302
|
+
const dir = this.defaultFolder;
|
|
303
|
+
if (!existsSync(dir)) return { folder: dir, docs: [] };
|
|
304
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
305
|
+
const docs = await Promise.all(
|
|
306
|
+
entries
|
|
307
|
+
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md'))
|
|
308
|
+
.map(async (e) => {
|
|
309
|
+
const stat = await fs.stat(path.join(dir, e.name));
|
|
310
|
+
const md = await fs.readFile(path.join(dir, e.name), 'utf8');
|
|
311
|
+
const { frontmatter } = parseFrontmatter(md);
|
|
312
|
+
const body = getBody(md);
|
|
313
|
+
return {
|
|
314
|
+
file: e.name,
|
|
315
|
+
title: (frontmatter as any).title || firstHeading(body) || e.name.replace(/\.md$/i, ''),
|
|
316
|
+
author: (frontmatter as any).author || '',
|
|
317
|
+
updatedAt: stat.mtime.toISOString(),
|
|
318
|
+
wordCount: body.split(/\s+/).filter(Boolean).length,
|
|
319
|
+
};
|
|
320
|
+
})
|
|
321
|
+
);
|
|
322
|
+
return { folder: dir, docs: docs.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Generate table of contents from the document structure
|
|
327
|
+
* @readOnly
|
|
328
|
+
*/
|
|
329
|
+
async toc() {
|
|
330
|
+
const body = getBody(await this.readDoc());
|
|
331
|
+
const outline = buildOutline(body);
|
|
332
|
+
const tocMarkdown = outline.map((h) => `${' '.repeat(h.level - 1)}- ${h.text}`).join('\n');
|
|
333
|
+
return { outline, markdown: tocMarkdown };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Document statistics: word count, reading time, section breakdown
|
|
338
|
+
* @readOnly
|
|
339
|
+
*/
|
|
340
|
+
async stats() {
|
|
341
|
+
const body = getBody(await this.readDoc());
|
|
342
|
+
return computeStats(body);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Private ─────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
private async readDoc(): Promise<string> {
|
|
348
|
+
try {
|
|
349
|
+
return await fs.readFile(this.docPath, 'utf8');
|
|
350
|
+
} catch {
|
|
351
|
+
return DEFAULT_DOC;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ── Pure Helpers ──────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
function parseFrontmatter(markdown: string): { frontmatter: Record<string, any>; body: string } {
|
|
359
|
+
const match = markdown.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
360
|
+
if (!match) return { frontmatter: {}, body: markdown };
|
|
361
|
+
const yamlText = match[1];
|
|
362
|
+
const body = markdown.slice(match[0].length);
|
|
363
|
+
// Simple YAML parser for flat/nested values
|
|
364
|
+
const frontmatter: Record<string, any> = {};
|
|
365
|
+
let currentKey = '';
|
|
366
|
+
let currentNested: Record<string, string> | null = null;
|
|
367
|
+
for (const line of yamlText.split('\n')) {
|
|
368
|
+
const nestedMatch = line.match(/^ (\w+):\s*(.+)/);
|
|
369
|
+
if (nestedMatch && currentKey) {
|
|
370
|
+
if (!currentNested) currentNested = {};
|
|
371
|
+
currentNested[nestedMatch[1]] = nestedMatch[2].replace(/^["']|["']$/g, '');
|
|
372
|
+
frontmatter[currentKey] = currentNested;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
|
|
376
|
+
if (kvMatch) {
|
|
377
|
+
if (currentNested) currentNested = null;
|
|
378
|
+
currentKey = kvMatch[1];
|
|
379
|
+
const val = kvMatch[2].replace(/^["']|["']$/g, '').trim();
|
|
380
|
+
if (val === 'true') frontmatter[currentKey] = true;
|
|
381
|
+
else if (val === 'false') frontmatter[currentKey] = false;
|
|
382
|
+
else if (val === '' || val === '""') frontmatter[currentKey] = '';
|
|
383
|
+
else if (/^\d+$/.test(val)) frontmatter[currentKey] = parseInt(val, 10);
|
|
384
|
+
else frontmatter[currentKey] = val;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { frontmatter, body };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function splitDoc(markdown: string): { frontmatterRaw: string; body: string } {
|
|
391
|
+
const match = markdown.match(/^(---\n[\s\S]*?\n---\n?)/);
|
|
392
|
+
if (!match) return { frontmatterRaw: '', body: markdown };
|
|
393
|
+
return { frontmatterRaw: match[1], body: markdown.slice(match[1].length) };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getBody(markdown: string): string {
|
|
397
|
+
return splitDoc(markdown).body;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
interface HeadingEntry {
|
|
401
|
+
level: number;
|
|
402
|
+
text: string;
|
|
403
|
+
line: number;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function buildOutline(body: string): HeadingEntry[] {
|
|
407
|
+
const headings: HeadingEntry[] = [];
|
|
408
|
+
const lines = body.split('\n');
|
|
409
|
+
for (let i = 0; i < lines.length; i++) {
|
|
410
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
|
|
411
|
+
if (match) {
|
|
412
|
+
headings.push({ level: match[1].length, text: match[2].trim(), line: i + 1 });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return headings;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function computeStats(body: string) {
|
|
419
|
+
const words = body.split(/\s+/).filter(Boolean).length;
|
|
420
|
+
const chars = body.length;
|
|
421
|
+
const paragraphs = body.split(/\n\n+/).filter((p) => p.trim().length > 0).length;
|
|
422
|
+
const headings = buildOutline(body);
|
|
423
|
+
const readingTime = Math.max(1, Math.ceil(words / 200));
|
|
424
|
+
|
|
425
|
+
// Per-section word counts
|
|
426
|
+
const sections = headings.map((h, i) => {
|
|
427
|
+
const start = body.indexOf(body.split('\n')[h.line - 1]);
|
|
428
|
+
const nextHeading = headings[i + 1];
|
|
429
|
+
const end = nextHeading ? body.indexOf(body.split('\n')[nextHeading.line - 1]) : body.length;
|
|
430
|
+
const sectionText = body.slice(start, end);
|
|
431
|
+
return {
|
|
432
|
+
heading: h.text,
|
|
433
|
+
level: h.level,
|
|
434
|
+
words: sectionText.split(/\s+/).filter(Boolean).length,
|
|
435
|
+
};
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return { words, chars, paragraphs, headings: headings.length, readingTime, sections };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function firstHeading(body: string): string {
|
|
442
|
+
return body.match(/^#\s+(.+)$/m)?.[1]?.trim() || '';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function extractSection(body: string, heading: string): string | null {
|
|
446
|
+
const lines = body.split('\n');
|
|
447
|
+
let startLine = -1;
|
|
448
|
+
let headingLevel = 0;
|
|
449
|
+
|
|
450
|
+
for (let i = 0; i < lines.length; i++) {
|
|
451
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
|
|
452
|
+
if (match && match[2].trim().toLowerCase() === heading.toLowerCase()) {
|
|
453
|
+
startLine = i;
|
|
454
|
+
headingLevel = match[1].length;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (startLine === -1) return null;
|
|
460
|
+
|
|
461
|
+
let endLine = lines.length;
|
|
462
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
463
|
+
const match = lines[i].match(/^(#{1,6})\s+/);
|
|
464
|
+
if (match && match[1].length <= headingLevel) {
|
|
465
|
+
endLine = i;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return lines.slice(startLine, endLine).join('\n');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function replaceSection(body: string, heading: string, newContent: string): string | null {
|
|
474
|
+
const lines = body.split('\n');
|
|
475
|
+
let startLine = -1;
|
|
476
|
+
let headingLevel = 0;
|
|
477
|
+
|
|
478
|
+
for (let i = 0; i < lines.length; i++) {
|
|
479
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
|
|
480
|
+
if (match && match[2].trim().toLowerCase() === heading.toLowerCase()) {
|
|
481
|
+
startLine = i;
|
|
482
|
+
headingLevel = match[1].length;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (startLine === -1) return null;
|
|
488
|
+
|
|
489
|
+
let endLine = lines.length;
|
|
490
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
491
|
+
const match = lines[i].match(/^(#{1,6})\s+/);
|
|
492
|
+
if (match && match[1].length <= headingLevel) {
|
|
493
|
+
endLine = i;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const before = lines.slice(0, startLine);
|
|
499
|
+
const after = lines.slice(endLine);
|
|
500
|
+
return [...before, newContent, ...after].join('\n');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function insertAfterSection(body: string, heading: string, content: string): string {
|
|
504
|
+
const lines = body.split('\n');
|
|
505
|
+
let headingLevel = 0;
|
|
506
|
+
let insertAt = lines.length;
|
|
507
|
+
|
|
508
|
+
for (let i = 0; i < lines.length; i++) {
|
|
509
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)/);
|
|
510
|
+
if (match && match[2].trim().toLowerCase() === heading.toLowerCase()) {
|
|
511
|
+
headingLevel = match[1].length;
|
|
512
|
+
// Find end of this section
|
|
513
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
514
|
+
const nextMatch = lines[j].match(/^(#{1,6})\s+/);
|
|
515
|
+
if (nextMatch && nextMatch[1].length <= headingLevel) {
|
|
516
|
+
insertAt = j;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (insertAt === lines.length) insertAt = lines.length;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const before = lines.slice(0, insertAt);
|
|
526
|
+
const after = lines.slice(insertAt);
|
|
527
|
+
return [...before, '', content, '', ...after].join('\n');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function fuzzyMatch(text: string, query: string): boolean {
|
|
531
|
+
let qi = 0;
|
|
532
|
+
for (let i = 0; i < text.length && qi < query.length; i++) {
|
|
533
|
+
if (text[i] === query[qi]) qi++;
|
|
534
|
+
}
|
|
535
|
+
return qi === query.length;
|
|
536
|
+
}
|