@bendyline/squisq-formats 1.3.1 → 1.4.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/chunk-6M7Z25LA.js +46 -0
- package/dist/chunk-6M7Z25LA.js.map +1 -0
- package/dist/{chunk-S3Y7H2BK.js → chunk-APSIAEXL.js} +9 -8
- package/dist/chunk-APSIAEXL.js.map +1 -0
- package/dist/{chunk-UDS45KUJ.js → chunk-BJQIJ4S4.js} +36 -52
- package/dist/chunk-BJQIJ4S4.js.map +1 -0
- package/dist/{chunk-33YRFXZZ.js → chunk-CRTC6DEX.js} +259 -13
- package/dist/chunk-CRTC6DEX.js.map +1 -0
- package/dist/chunk-FMOIGA5T.js +201 -0
- package/dist/chunk-FMOIGA5T.js.map +1 -0
- package/dist/{chunk-VN2KEOYB.js → chunk-I2GFTEOV.js} +321 -91
- package/dist/chunk-I2GFTEOV.js.map +1 -0
- package/dist/chunk-JBNVE2GV.js +116 -0
- package/dist/chunk-JBNVE2GV.js.map +1 -0
- package/dist/chunk-L53YIGYS.js +141 -0
- package/dist/chunk-L53YIGYS.js.map +1 -0
- package/dist/{chunk-ERZ627GR.js → chunk-ON232HJR.js} +11 -26
- package/dist/chunk-ON232HJR.js.map +1 -0
- package/dist/chunk-PN52A5AA.js +32 -0
- package/dist/chunk-PN52A5AA.js.map +1 -0
- package/dist/{chunk-7DEUGIOO.js → chunk-QSE7EWE7.js} +12 -87
- package/dist/chunk-QSE7EWE7.js.map +1 -0
- package/dist/csv/index.d.ts +37 -0
- package/dist/csv/index.js +13 -0
- package/dist/csv/index.js.map +1 -0
- package/dist/docx/index.js +5 -3
- package/dist/epub/index.js +2 -1
- package/dist/html/index.d.ts +39 -2
- package/dist/html/index.js +7 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +31 -13
- package/dist/ooxml/index.js +16 -16
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.d.ts +20 -18
- package/dist/pptx/index.js +6 -2
- package/dist/xlsx/index.d.ts +20 -19
- package/dist/xlsx/index.js +2 -1
- package/package.json +7 -2
- package/src/__tests__/csvImport.test.ts +51 -0
- package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
- package/src/__tests__/htmlImport.test.ts +57 -0
- package/src/__tests__/plainHtml.test.ts +31 -0
- package/src/__tests__/pptxExport.test.ts +138 -0
- package/src/__tests__/pptxImport.test.ts +70 -0
- package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
- package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
- package/src/__tests__/roundTripMatrix.test.ts +136 -0
- package/src/__tests__/xlsxImport.test.ts +80 -0
- package/src/csv/index.ts +165 -0
- package/src/docx/export.ts +27 -44
- package/src/docx/import.ts +1 -2
- package/src/epub/export.ts +10 -27
- package/src/html/import.ts +297 -0
- package/src/html/index.ts +4 -0
- package/src/html/plainHtml.ts +66 -16
- package/src/index.ts +10 -3
- package/src/ooxml/namespaces.ts +3 -0
- package/src/pdf/export.ts +10 -5
- package/src/pdf/import.ts +1 -1
- package/src/pptx/export.ts +225 -85
- package/src/pptx/import.ts +156 -0
- package/src/pptx/index.ts +10 -28
- package/src/shared/inlineRuns.ts +99 -0
- package/src/shared/text.ts +41 -0
- package/src/xlsx/import.ts +159 -0
- package/src/xlsx/index.ts +11 -28
- package/dist/chunk-33YRFXZZ.js.map +0 -1
- package/dist/chunk-67KIJHV2.js +0 -21
- package/dist/chunk-67KIJHV2.js.map +0 -1
- package/dist/chunk-7DEUGIOO.js.map +0 -1
- package/dist/chunk-ERZ627GR.js.map +0 -1
- package/dist/chunk-S3Y7H2BK.js.map +0 -1
- package/dist/chunk-UDS45KUJ.js.map +0 -1
- package/dist/chunk-VN2KEOYB.js.map +0 -1
- package/dist/chunk-YN5HFCEW.js +0 -120
- package/dist/chunk-YN5HFCEW.js.map +0 -1
package/src/html/plainHtml.ts
CHANGED
|
@@ -14,9 +14,18 @@
|
|
|
14
14
|
* blob URLs (live preview) or data URIs (single-file export).
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
sanitizeHtmlNodes,
|
|
19
|
+
sanitizeUrl,
|
|
20
|
+
type HtmlPolicy,
|
|
21
|
+
type MarkdownDocument,
|
|
22
|
+
type MarkdownNode,
|
|
23
|
+
type HtmlNode,
|
|
24
|
+
readFrontmatterThemeId,
|
|
25
|
+
} from '@bendyline/squisq/markdown';
|
|
18
26
|
import type { Theme } from '@bendyline/squisq/schemas';
|
|
19
|
-
import { resolveFontFamily, buildGoogleFontsUrl
|
|
27
|
+
import { resolveFontFamily, buildGoogleFontsUrl } from '@bendyline/squisq/schemas';
|
|
28
|
+
import { resolveThemeForDoc } from '@bendyline/squisq/doc';
|
|
20
29
|
|
|
21
30
|
// ── Public Types ───────────────────────────────────────────────────
|
|
22
31
|
|
|
@@ -71,6 +80,11 @@ export interface PlainHtmlExportOptions {
|
|
|
71
80
|
* option is not provided.
|
|
72
81
|
*/
|
|
73
82
|
iconsCss?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Raw HTML policy. Defaults to `sanitize`, which removes unsafe tags,
|
|
85
|
+
* event handlers, and executable URL schemes before emitting HTML.
|
|
86
|
+
*/
|
|
87
|
+
htmlPolicy?: HtmlPolicy;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
/**
|
|
@@ -81,6 +95,7 @@ export interface PlainHtmlExportOptions {
|
|
|
81
95
|
interface RenderCtx {
|
|
82
96
|
images?: Map<string, string>;
|
|
83
97
|
links?: Map<string, string>;
|
|
98
|
+
htmlPolicy: HtmlPolicy;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
// ── Public API ─────────────────────────────────────────────────────
|
|
@@ -97,19 +112,20 @@ export function markdownDocToPlainHtml(
|
|
|
97
112
|
doc: MarkdownDocument,
|
|
98
113
|
options: PlainHtmlExportOptions = {},
|
|
99
114
|
): string {
|
|
100
|
-
const { title = 'Document', images, links, themeId, iconsCss } = options;
|
|
115
|
+
const { title = 'Document', images, links, themeId, iconsCss, htmlPolicy = 'sanitize' } = options;
|
|
101
116
|
// Fall back chain for theme: explicit `theme` → explicit `themeId`
|
|
102
117
|
// option → doc frontmatter `themeId`. Hosts whose export dialog
|
|
103
118
|
// tracks themes by id can pass `themeId` straight through; authored
|
|
104
119
|
// docs with `themeId: warm-earth` in frontmatter get styled
|
|
105
120
|
// automatically when neither is supplied.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
121
|
+
// Resolution is doc-scoped via `resolveThemeForDoc`, so an inline
|
|
122
|
+
// `squisq-custom-themes` id resolves here with no global registration —
|
|
123
|
+
// and `readFrontmatterThemeId` picks up the editor's canonical
|
|
124
|
+
// `squisq-theme` key. Stays undefined when nothing selects a theme so
|
|
125
|
+
// un-themed exports render unstyled (unchanged behavior).
|
|
126
|
+
const resolveId = themeId ?? readFrontmatterThemeId(doc.frontmatter);
|
|
127
|
+
const theme = options.theme ?? (resolveId ? resolveThemeForDoc(doc, resolveId) : undefined);
|
|
128
|
+
const ctx: RenderCtx = { images, links, htmlPolicy };
|
|
113
129
|
const body = renderTopLevel(doc.children, ctx);
|
|
114
130
|
const fontsLink = theme ? renderFontsLink(theme) : '';
|
|
115
131
|
// Resolve how to load FontAwesome — only when the doc actually uses
|
|
@@ -258,7 +274,9 @@ interface FeaturedImage {
|
|
|
258
274
|
|
|
259
275
|
function renderFeatureImage(img: FeaturedImage, ctx: RenderCtx | undefined): string {
|
|
260
276
|
const resolved = ctx?.images?.get(img.src) ?? img.src;
|
|
261
|
-
const
|
|
277
|
+
const safeSrc = sanitizeUrl(resolved, 'media');
|
|
278
|
+
if (!safeSrc) return '<div class="squisq-feature__media squisq-feature__media--empty"></div>';
|
|
279
|
+
const attrs = [`src="${escapeAttr(safeSrc)}"`, `alt="${escapeAttr(img.alt)}"`];
|
|
262
280
|
// Emit `width` / `height` attributes only when the source HTML had
|
|
263
281
|
// them. The CSS rules then know to honor those values rather than
|
|
264
282
|
// stretching the image to fill the column.
|
|
@@ -604,12 +622,16 @@ function nodeToHtml(node: MarkdownNode | undefined | null, ctx?: RenderCtx): str
|
|
|
604
622
|
case 'link': {
|
|
605
623
|
const original = node.url ?? '';
|
|
606
624
|
const rewritten = ctx?.links?.get(original) ?? original;
|
|
607
|
-
|
|
625
|
+
const safeHref = sanitizeUrl(rewritten, 'link');
|
|
626
|
+
if (!safeHref) return childrenToHtml(node, ctx);
|
|
627
|
+
return `<a href="${escapeAttr(safeHref)}">${childrenToHtml(node, ctx)}</a>`;
|
|
608
628
|
}
|
|
609
629
|
case 'image': {
|
|
610
630
|
const original = node.url ?? '';
|
|
611
631
|
const resolved = ctx?.images?.get(original) ?? original;
|
|
612
|
-
|
|
632
|
+
const safeSrc = sanitizeUrl(resolved, 'media');
|
|
633
|
+
if (!safeSrc) return escapeHtml(node.alt ?? '');
|
|
634
|
+
return `<img src="${escapeAttr(safeSrc)}" alt="${escapeAttr(node.alt ?? '')}" />`;
|
|
613
635
|
}
|
|
614
636
|
case 'thematicBreak':
|
|
615
637
|
return '<hr />';
|
|
@@ -626,11 +648,12 @@ function nodeToHtml(node: MarkdownNode | undefined | null, ctx?: RenderCtx): str
|
|
|
626
648
|
}
|
|
627
649
|
case 'htmlBlock':
|
|
628
650
|
case 'htmlInline':
|
|
651
|
+
if (resolveHtmlPolicy(ctx) === 'strip') return '';
|
|
629
652
|
// Resized images and other authored HTML survive the round-trip
|
|
630
653
|
// as parsed `htmlChildren` — rewriting `<img src>` through the
|
|
631
654
|
// image map keeps the preview consistent with the markdown-image
|
|
632
655
|
// path. Other tags pass through unmodified.
|
|
633
|
-
return htmlChildrenToHtml(node.htmlChildren, ctx);
|
|
656
|
+
return htmlChildrenToHtml(resolveHtmlNodes(node.htmlChildren, ctx), ctx);
|
|
634
657
|
default: {
|
|
635
658
|
// Unknown / unhandled node — recurse into children if any so we
|
|
636
659
|
// don't drop content (e.g. directives, footnotes).
|
|
@@ -682,18 +705,28 @@ function tableToHtml(
|
|
|
682
705
|
return parts.join('');
|
|
683
706
|
}
|
|
684
707
|
|
|
708
|
+
function resolveHtmlPolicy(ctx: RenderCtx | undefined): HtmlPolicy {
|
|
709
|
+
return ctx?.htmlPolicy ?? 'sanitize';
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function resolveHtmlNodes(nodes: HtmlNode[] | undefined, ctx: RenderCtx | undefined): HtmlNode[] {
|
|
713
|
+
if (!nodes || nodes.length === 0) return [];
|
|
714
|
+
return resolveHtmlPolicy(ctx) === 'trusted' ? nodes : sanitizeHtmlNodes(nodes);
|
|
715
|
+
}
|
|
716
|
+
|
|
685
717
|
function htmlChildrenToHtml(nodes: HtmlNode[] | undefined, ctx?: RenderCtx): string {
|
|
686
718
|
if (!nodes || nodes.length === 0) return '';
|
|
719
|
+
const trusted = resolveHtmlPolicy(ctx) === 'trusted';
|
|
687
720
|
const out: string[] = [];
|
|
688
721
|
for (const node of nodes) {
|
|
689
722
|
if (node.type === 'htmlText') {
|
|
690
723
|
// HtmlText already represents authored HTML — emit verbatim so
|
|
691
724
|
// entity references the user wrote (e.g. `&`) survive.
|
|
692
|
-
out.push(node.value);
|
|
725
|
+
out.push(trusted ? node.value : escapeHtml(node.value));
|
|
693
726
|
continue;
|
|
694
727
|
}
|
|
695
728
|
if (node.type === 'htmlComment') {
|
|
696
|
-
out.push(`<!--${node.value}-->`);
|
|
729
|
+
if (trusted) out.push(`<!--${node.value}-->`);
|
|
697
730
|
continue;
|
|
698
731
|
}
|
|
699
732
|
// htmlElement
|
|
@@ -713,6 +746,23 @@ function htmlChildrenToHtml(nodes: HtmlNode[] | undefined, ctx?: RenderCtx): str
|
|
|
713
746
|
if ((tag === 'video' || tag === 'audio') && typeof attrs.poster === 'string') {
|
|
714
747
|
attrs.poster = ctx?.images?.get(attrs.poster) ?? attrs.poster;
|
|
715
748
|
}
|
|
749
|
+
if (!trusted) {
|
|
750
|
+
if (typeof attrs.src === 'string') {
|
|
751
|
+
const safeSrc = sanitizeUrl(attrs.src, 'media');
|
|
752
|
+
if (!safeSrc) delete attrs.src;
|
|
753
|
+
else attrs.src = safeSrc;
|
|
754
|
+
}
|
|
755
|
+
if (typeof attrs.poster === 'string') {
|
|
756
|
+
const safePoster = sanitizeUrl(attrs.poster, 'media');
|
|
757
|
+
if (!safePoster) delete attrs.poster;
|
|
758
|
+
else attrs.poster = safePoster;
|
|
759
|
+
}
|
|
760
|
+
if (typeof attrs.href === 'string') {
|
|
761
|
+
const safeHref = sanitizeUrl(attrs.href, 'link');
|
|
762
|
+
if (!safeHref) delete attrs.href;
|
|
763
|
+
else attrs.href = safeHref;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
716
766
|
const attrStr = Object.entries(attrs)
|
|
717
767
|
.map(([k, v]) => ` ${k}="${escapeAttr(v)}"`)
|
|
718
768
|
.join('');
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* Supported formats:
|
|
8
8
|
* - **DOCX** — Microsoft Word (import + export) ✅
|
|
9
9
|
* - **PDF** — Portable Document Format (import + export) ✅
|
|
10
|
-
* - **PPTX** — Microsoft PowerPoint (export ✅, import
|
|
11
|
-
* - **XLSX** — Microsoft Excel (planned)
|
|
10
|
+
* - **PPTX** — Microsoft PowerPoint (export ✅, import ✅)
|
|
11
|
+
* - **XLSX** — Microsoft Excel (import ✅, export planned)
|
|
12
|
+
* - **CSV** — Comma-separated values (import ✅, export ✅)
|
|
12
13
|
*
|
|
13
14
|
* All converters run in the browser — no server or native binaries required.
|
|
14
15
|
* The shared `ooxml/` subpath export provides reusable OOXML infrastructure.
|
|
@@ -32,10 +33,14 @@ export type { DocxExportOptions, DocxImportOptions } from './docx/index.js';
|
|
|
32
33
|
export { markdownDocToPptx, docToPptx, pptxToMarkdownDoc, pptxToDoc } from './pptx/index.js';
|
|
33
34
|
export type { PptxExportOptions, PptxImportOptions } from './pptx/index.js';
|
|
34
35
|
|
|
35
|
-
// XLSX (stub)
|
|
36
|
+
// XLSX (import implemented, export stub)
|
|
36
37
|
export { markdownDocToXlsx, docToXlsx, xlsxToMarkdownDoc, xlsxToDoc } from './xlsx/index.js';
|
|
37
38
|
export type { XlsxExportOptions, XlsxImportOptions } from './xlsx/index.js';
|
|
38
39
|
|
|
40
|
+
// CSV (import + export)
|
|
41
|
+
export { csvToMarkdownDoc, csvToDoc, markdownDocToCsv, parseCsv } from './csv/index.js';
|
|
42
|
+
export type { CsvImportOptions, CsvExportOptions } from './csv/index.js';
|
|
43
|
+
|
|
39
44
|
// PDF (fully implemented)
|
|
40
45
|
export {
|
|
41
46
|
markdownDocToPdf,
|
|
@@ -49,6 +54,8 @@ export type { PdfExportOptions, PdfImportOptions } from './pdf/index.js';
|
|
|
49
54
|
// HTML (fully implemented)
|
|
50
55
|
export { docToHtml, docToHtmlZip, collectImagePaths } from './html/index.js';
|
|
51
56
|
export type { HtmlExportOptions, HtmlZipExportOptions } from './html/index.js';
|
|
57
|
+
export { htmlToMarkdown, htmlToMarkdownDoc, htmlToMarkdownDocSync } from './html/index.js';
|
|
58
|
+
export type { HtmlImportOptions } from './html/index.js';
|
|
52
59
|
|
|
53
60
|
// EPUB (export)
|
|
54
61
|
export { markdownDocToEpub, docToEpub } from './epub/index.js';
|
package/src/ooxml/namespaces.ts
CHANGED
|
@@ -77,6 +77,9 @@ export const NS_WML = 'http://schemas.openxmlformats.org/wordprocessingml/2006/m
|
|
|
77
77
|
/** PresentationML main namespace (p:) */
|
|
78
78
|
export const NS_PML = 'http://schemas.openxmlformats.org/presentationml/2006/main';
|
|
79
79
|
|
|
80
|
+
/** PowerPoint 2010 extension namespace (p14:) */
|
|
81
|
+
export const NS_PML_2010 = 'http://schemas.microsoft.com/office/powerpoint/2010/main';
|
|
82
|
+
|
|
80
83
|
// ============================================
|
|
81
84
|
// SpreadsheetML (XLSX)
|
|
82
85
|
// ============================================
|
package/src/pdf/export.ts
CHANGED
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
import { PDFDocument, StandardFonts, rgb, PDFFont, PDFPage } from 'pdf-lib';
|
|
23
23
|
|
|
24
24
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
25
|
-
import {
|
|
26
|
-
import { docToMarkdown } from '@bendyline/squisq/doc';
|
|
25
|
+
import { docToMarkdown, resolveThemeForDoc } from '@bendyline/squisq/doc';
|
|
27
26
|
import type {
|
|
28
27
|
MarkdownDocument,
|
|
29
28
|
MarkdownBlockNode,
|
|
@@ -51,6 +50,7 @@ import type {
|
|
|
51
50
|
MarkdownInlineMath,
|
|
52
51
|
MarkdownFootnoteReference,
|
|
53
52
|
} from '@bendyline/squisq/markdown';
|
|
53
|
+
import { readFrontmatterThemeId } from '@bendyline/squisq/markdown';
|
|
54
54
|
|
|
55
55
|
import {
|
|
56
56
|
PAGE_WIDTH_LETTER,
|
|
@@ -127,7 +127,7 @@ export async function markdownDocToPdf(
|
|
|
127
127
|
pdfDoc.setCreationDate(new Date());
|
|
128
128
|
pdfDoc.setModificationDate(new Date());
|
|
129
129
|
|
|
130
|
-
const ctx = await createExportContext(pdfDoc, options);
|
|
130
|
+
const ctx = await createExportContext(pdfDoc, options, doc);
|
|
131
131
|
|
|
132
132
|
renderBlocks(doc.children, ctx, 0);
|
|
133
133
|
|
|
@@ -190,6 +190,7 @@ interface ExportContext {
|
|
|
190
190
|
async function createExportContext(
|
|
191
191
|
pdfDoc: PDFDocument,
|
|
192
192
|
options: PdfExportOptions,
|
|
193
|
+
doc: MarkdownDocument,
|
|
193
194
|
): Promise<ExportContext> {
|
|
194
195
|
const [regular, bold, italic, boldItalic, mono, monoBold] = await Promise.all([
|
|
195
196
|
pdfDoc.embedFont(StandardFonts.Helvetica),
|
|
@@ -213,8 +214,12 @@ async function createExportContext(
|
|
|
213
214
|
let colorHeading = COLOR_HEADING;
|
|
214
215
|
let colorLink = COLOR_LINK;
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
// Honor an explicit themeId, else the doc's frontmatter theme (`squisq-theme`
|
|
218
|
+
// / legacy) — mirroring PPTX/DOCX/HTML so every format themes consistently,
|
|
219
|
+
// including inline custom themes (resolved doc-scoped).
|
|
220
|
+
const themeId = options.themeId ?? readFrontmatterThemeId(doc.frontmatter);
|
|
221
|
+
if (themeId) {
|
|
222
|
+
const theme = resolveThemeForDoc(doc, themeId);
|
|
218
223
|
if (theme.colors) {
|
|
219
224
|
colorText = hexToRgb(theme.colors.text) ?? COLOR_TEXT;
|
|
220
225
|
colorHeading = hexToRgb(theme.colors.primary) ?? COLOR_HEADING;
|
package/src/pdf/import.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import type { Doc } from '@bendyline/squisq/schemas';
|
|
24
24
|
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
25
|
+
import { stringifyMarkdown } from '@bendyline/squisq/markdown';
|
|
25
26
|
import type {
|
|
26
27
|
MarkdownDocument,
|
|
27
28
|
MarkdownBlockNode,
|
|
@@ -169,7 +170,6 @@ export async function pdfToContainer(
|
|
|
169
170
|
|
|
170
171
|
const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
|
|
171
172
|
|
|
172
|
-
const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
|
|
173
173
|
const markdown = stringifyMarkdown(markdownDoc);
|
|
174
174
|
|
|
175
175
|
const container = new MemoryContentContainer();
|