@gpdoc/filekit 1.23.0 → 1.24.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +86 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gpdoc/filekit",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "type": "module",
5
5
  "description": "Headless GPDoc file format detection, validation, and text conversion",
6
6
  "repository": "https://github.com/repetere/gpdoc.git",
package/src/index.js CHANGED
@@ -13,6 +13,52 @@ const FRONT_MATTER = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
13
13
  const MAX_URL_RESPONSE_BYTES = 10 * 1024 * 1024;
14
14
  const URL_TIMEOUT_MS = 15_000;
15
15
 
16
+ export const GPDOC_PAGE_BREAK = '<div style="page-break-after: always;"></div>';
17
+
18
+ export function resolveSlideDelimiter(metadata = {}) {
19
+ const configured = typeof metadata?.slideDelimiter === 'string'
20
+ ? metadata.slideDelimiter.trim()
21
+ : '';
22
+ return configured || GPDOC_PAGE_BREAK;
23
+ }
24
+
25
+ export function splitMarkdownSlides(markdown, delimiter = GPDOC_PAGE_BREAK) {
26
+ // @spec FILE-MODEL-006, FILE-MODEL-007, FILE-MODEL-008, FILE-MODEL-009
27
+ const boundary = typeof delimiter === 'string' && delimiter.trim()
28
+ ? delimiter.trim()
29
+ : GPDOC_PAGE_BREAK;
30
+ const slides = [];
31
+ let current = [];
32
+ let fence = null;
33
+ for (const line of String(markdown ?? '').replace(/\r\n/g, '\n').split('\n')) {
34
+ const marker = line.match(/^\s*(`{3,}|~{3,})/u)?.[1] ?? null;
35
+ if (marker) {
36
+ if (!fence) fence = { character: marker[0], length: marker.length };
37
+ else if (marker[0] === fence.character && marker.length >= fence.length) fence = null;
38
+ current.push(line);
39
+ continue;
40
+ }
41
+ if (!fence && line.trim() === boundary) {
42
+ slides.push(current.join('\n').trim());
43
+ current = [];
44
+ continue;
45
+ }
46
+ current.push(line);
47
+ }
48
+ slides.push(current.join('\n').trim());
49
+ return slides;
50
+ }
51
+
52
+ export function joinMarkdownSlides(slides, delimiter = GPDOC_PAGE_BREAK) {
53
+ // @spec FILE-MODEL-007, FILE-MODEL-008, FILE-MODEL-009
54
+ const boundary = typeof delimiter === 'string' && delimiter.trim()
55
+ ? delimiter.trim()
56
+ : GPDOC_PAGE_BREAK;
57
+ return (Array.isArray(slides) ? slides : [slides ?? ''])
58
+ .map((slide) => String(slide ?? '').trim())
59
+ .join(`\n\n${boundary}\n\n`);
60
+ }
61
+
16
62
  export class FileKitError extends Error {
17
63
  constructor(code, message) {
18
64
  super(message);
@@ -284,8 +330,15 @@ function parseGpdocJson(source) {
284
330
  if (!isManagedMetadata(json) || !isPlainObject(json.content)) {
285
331
  throw new FileKitError('INVALID_GPDOC', 'Managed GPDoc metadata must include id, filename, and a supported filetype.');
286
332
  }
333
+ if (json.filetype === 'slides' && Array.isArray(json.content.slides)) {
334
+ return {
335
+ metadata: clone(json),
336
+ body: joinMarkdownSlides(json.content.slides, resolveSlideDelimiter(json)),
337
+ slides: json.content.slides.map((slide) => String(slide ?? '')),
338
+ };
339
+ }
287
340
  if (typeof json.content.document !== 'string') {
288
- throw new FileKitError('INVALID_GPDOC', 'This CLI release supports document content only.');
341
+ throw new FileKitError('INVALID_GPDOC', 'This GPDoc file does not contain supported document or slide content.');
289
342
  }
290
343
  return { metadata: clone(json), body: json.content.document };
291
344
  }
@@ -306,11 +359,15 @@ export function readDocument(source, filePath = '') {
306
359
  }
307
360
  if (format === 'json') {
308
361
  const parsed = parseGpdocJson(source);
309
- return { format: 'gpdoc-json', managed: true, filetype: parsed.metadata.filetype, title: parsed.metadata.filename, body: parsed.body, metadata: parsed.metadata, warnings: [] };
362
+ return { format: 'gpdoc-json', managed: true, filetype: parsed.metadata.filetype, title: parsed.metadata.filename, body: parsed.body, slides: parsed.slides, metadata: parsed.metadata, warnings: [] };
310
363
  }
311
364
  const parsed = parseManagedMarkdown(String(source));
312
365
  if (parsed) {
313
- return { format: 'gpdoc-markdown', managed: true, filetype: parsed.metadata.filetype, title: parsed.metadata.filename, body: parsed.body, metadata: parsed.metadata, warnings: [] };
366
+ const document = { format: 'gpdoc-markdown', managed: true, filetype: parsed.metadata.filetype, title: parsed.metadata.filename, body: parsed.body, metadata: parsed.metadata, warnings: [] };
367
+ if (parsed.metadata.filetype === 'slides') {
368
+ document.slides = splitMarkdownSlides(parsed.body, resolveSlideDelimiter(parsed.metadata));
369
+ }
370
+ return document;
314
371
  }
315
372
  return { format: 'markdown', managed: false, filetype: 'document', title: filenameTitle(filePath), body: String(source), warnings: [] };
316
373
  }
@@ -327,6 +384,22 @@ export function createManagedDocument(title = 'Untitled', body = '') {
327
384
  };
328
385
  }
329
386
 
387
+ export function createManagedSlides(title = 'Untitled', slides = ['# Slide 1']) {
388
+ // @spec CLI-074, FILE-MODEL-011
389
+ const now = new Date().toISOString();
390
+ const normalizedSlides = (Array.isArray(slides) && slides.length ? slides : ['# Slide 1'])
391
+ .map((slide) => String(slide ?? ''));
392
+ return {
393
+ id: `file_${randomUUID()}`,
394
+ filename: title,
395
+ filetype: 'slides',
396
+ slideDelimiter: GPDOC_PAGE_BREAK,
397
+ created: now,
398
+ updated: now,
399
+ content: { slides: normalizedSlides },
400
+ };
401
+ }
402
+
330
403
  export function serializeManagedMarkdown(metadata, body) {
331
404
  // @spec CLI-001, CLI-066
332
405
  // Markdown has a separate body. `content.document` is reserved for GPDoc JSON.
@@ -347,7 +420,16 @@ function markdownToText(markdown) {
347
420
  export function convertDocument(document, targetFormat) {
348
421
  const target = String(targetFormat).toLowerCase();
349
422
  const metadata = document.managed ? clone(document.metadata) : createManagedDocument(document.title, document.body);
350
- metadata.content = { document: document.body };
423
+ if (document.filetype === 'slides') {
424
+ metadata.slideDelimiter = resolveSlideDelimiter(metadata);
425
+ metadata.content = {
426
+ slides: Array.isArray(document.slides)
427
+ ? document.slides.map((slide) => String(slide ?? ''))
428
+ : splitMarkdownSlides(document.body, metadata.slideDelimiter),
429
+ };
430
+ } else {
431
+ metadata.content = { document: document.body };
432
+ }
351
433
  if (target === 'gpdoc') return serializeManagedMarkdown(metadata, document.body);
352
434
  if (target === 'markdown' || target === 'md') return document.body;
353
435
  if (target === 'html') return `<!doctype html>\n<html><head><meta charset="utf-8"><title>${escapeHtml(document.title)}</title></head><body>\n${marked.parse(document.body)}\n</body></html>\n`;