@ohos-ports/slides-grab 1.5.0-beta.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/LICENSE +21 -0
- package/README-ko.md +292 -0
- package/README.md +301 -0
- package/bin/ohos-postinstall.cjs +118 -0
- package/bin/ppt-agent.js +620 -0
- package/convert.cjs +20 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
- package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
- package/native/@img/sharp-openharmony-arm64/package.json +16 -0
- package/package.json +95 -0
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/build-viewer.js +444 -0
- package/scripts/design-gate.js +258 -0
- package/scripts/download-video.js +213 -0
- package/scripts/editor-server.js +1167 -0
- package/scripts/figma-export.js +169 -0
- package/scripts/generate-image.js +116 -0
- package/scripts/generate-images.js +164 -0
- package/scripts/html2pdf.js +822 -0
- package/scripts/html2png.js +246 -0
- package/scripts/html2pptx.js +162 -0
- package/scripts/import-template.js +86 -0
- package/scripts/install-runtime.js +216 -0
- package/scripts/render-tldraw.js +44 -0
- package/scripts/validate-slides.js +221 -0
- package/skills/slides-grab/SKILL.md +65 -0
- package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +88 -0
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +76 -0
- package/skills/slides-grab-design/references/design-system-full.md +568 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
- package/skills/slides-grab-export/SKILL.md +55 -0
- package/skills/slides-grab-export/references/export-rules.md +27 -0
- package/skills/slides-grab-export/references/html2pptx.md +627 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
- package/skills/slides-grab-html/SKILL.md +67 -0
- package/skills/slides-grab-image/SKILL.md +82 -0
- package/skills/slides-grab-plan/SKILL.md +68 -0
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/skills/slides-grab-plan/references/outline-format.md +47 -0
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
- package/src/codex-imagen.js +182 -0
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +329 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +209 -0
- package/src/editor/codex-edit.js +584 -0
- package/src/editor/edit-subprocess.js +170 -0
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/editor.html +1785 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +238 -0
- package/src/editor/js/editor-dom.js +59 -0
- package/src/editor/js/editor-init.js +405 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +558 -0
- package/src/editor/js/editor-send.js +175 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +41 -0
- package/src/editor/js/editor-type.js +71 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/js/model-registry.js +37 -0
- package/src/editor/screenshot.js +82 -0
- package/src/export-resolution.cjs +68 -0
- package/src/figma.js +71 -0
- package/src/html2pptx-scale.cjs +120 -0
- package/src/html2pptx.cjs +1262 -0
- package/src/image-contract.js +329 -0
- package/src/image-native.js +468 -0
- package/src/nano-banana.js +841 -0
- package/src/ohos-browser-bridge.cjs +272 -0
- package/src/pptx-raster-export.cjs +299 -0
- package/src/resolve.js +110 -0
- package/src/slide-mode.cjs +72 -0
- package/src/template-fidelity.js +267 -0
- package/src/template-import.js +505 -0
- package/src/template-layout.js +261 -0
- package/src/template-pack.js +255 -0
- package/src/tldraw/render.js +473 -0
- package/src/validation/cli.js +120 -0
- package/src/validation/core.js +1023 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/design-styles/README.md +20 -0
- package/templates/design-styles/preview.html +4438 -0
- package/templates/diagram-tldraw.html +56 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +43 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const DEFAULT_SLIDE_MODE = 'presentation';
|
|
2
|
+
const PT_TO_PX = 96 / 72;
|
|
3
|
+
|
|
4
|
+
const SLIDE_MODES = Object.freeze({
|
|
5
|
+
presentation: Object.freeze({
|
|
6
|
+
name: 'presentation',
|
|
7
|
+
framePt: Object.freeze({ width: 720, height: 405 }),
|
|
8
|
+
framePx: Object.freeze({ width: 720 * PT_TO_PX, height: 405 * PT_TO_PX }),
|
|
9
|
+
screenshotPx: Object.freeze({ width: 1600, height: 900 }),
|
|
10
|
+
pptxSizeIn: Object.freeze({ width: 13.33, height: 7.5 }),
|
|
11
|
+
figmaSizeIn: Object.freeze({ width: 10, height: 5.625 }),
|
|
12
|
+
sizeLabel: '720pt x 405pt',
|
|
13
|
+
coordinateSpaceLabel: '960x540',
|
|
14
|
+
aspectRatioLabel: '16:9',
|
|
15
|
+
}),
|
|
16
|
+
'card-news': Object.freeze({
|
|
17
|
+
name: 'card-news',
|
|
18
|
+
framePt: Object.freeze({ width: 720, height: 720 }),
|
|
19
|
+
framePx: Object.freeze({ width: 720 * PT_TO_PX, height: 720 * PT_TO_PX }),
|
|
20
|
+
screenshotPx: Object.freeze({ width: 1600, height: 1600 }),
|
|
21
|
+
pptxSizeIn: Object.freeze({ width: 10, height: 10 }),
|
|
22
|
+
figmaSizeIn: Object.freeze({ width: 10, height: 10 }),
|
|
23
|
+
sizeLabel: '720pt x 720pt',
|
|
24
|
+
coordinateSpaceLabel: '960x960',
|
|
25
|
+
aspectRatioLabel: '1:1',
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function getSlideModeChoices() {
|
|
30
|
+
return Object.keys(SLIDE_MODES);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSlideMode(value, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
allowEmpty = false,
|
|
36
|
+
optionName = '--mode',
|
|
37
|
+
} = options;
|
|
38
|
+
|
|
39
|
+
if (typeof value !== 'string') {
|
|
40
|
+
if (allowEmpty) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`${optionName} must be one of: ${getSlideModeChoices().join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalized = value.trim().toLowerCase();
|
|
47
|
+
if (normalized === '') {
|
|
48
|
+
if (allowEmpty) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`${optionName} must be one of: ${getSlideModeChoices().join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!SLIDE_MODES[normalized]) {
|
|
55
|
+
throw new Error(`Unknown ${optionName} value: ${value}. Expected one of: ${getSlideModeChoices().join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getSlideModeConfig(value = DEFAULT_SLIDE_MODE) {
|
|
62
|
+
return SLIDE_MODES[normalizeSlideMode(value, { optionName: '--mode' })];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
DEFAULT_SLIDE_MODE,
|
|
67
|
+
PT_TO_PX,
|
|
68
|
+
SLIDE_MODES,
|
|
69
|
+
getSlideModeChoices,
|
|
70
|
+
getSlideModeConfig,
|
|
71
|
+
normalizeSlideMode,
|
|
72
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { TemplatePackError, TEMPLATE_PACK_RELATIVE_PATH, loadTemplatePackFromSlidesDir } from './template-pack.js';
|
|
6
|
+
|
|
7
|
+
const METADATA_RE = /<!--\s*slides-grab-template:\s*(\{[\s\S]*?\})\s*-->/i;
|
|
8
|
+
const COLOR_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
|
9
|
+
const FONT_RE = /font-family\s*:\s*([^;}"'<]+)/gi;
|
|
10
|
+
|
|
11
|
+
export function templatePackExists(slidesDir) {
|
|
12
|
+
return existsSync(join(slidesDir, TEMPLATE_PACK_RELATIVE_PATH));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseTemplateMetadata(html) {
|
|
16
|
+
const match = METADATA_RE.exec(html || '');
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(match[1]);
|
|
20
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
21
|
+
return {
|
|
22
|
+
layoutId: parsed.layoutId ?? parsed.layout_id ?? null,
|
|
23
|
+
layoutKind: parsed.layoutKind ?? parsed.layout_kind ?? null,
|
|
24
|
+
};
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function buildTemplateFidelityReport({ slidesDir, slideFiles, previewRelativeDir }) {
|
|
31
|
+
if (!templatePackExists(slidesDir)) {
|
|
32
|
+
return {
|
|
33
|
+
status: 'not-applicable',
|
|
34
|
+
slides: [],
|
|
35
|
+
findings: [],
|
|
36
|
+
referencePreviewFiles: [],
|
|
37
|
+
markdown: '',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let pack;
|
|
42
|
+
try {
|
|
43
|
+
pack = loadTemplatePackFromSlidesDir(slidesDir, { optional: true });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof TemplatePackError) {
|
|
46
|
+
return buildFailedReport({
|
|
47
|
+
findings: [{
|
|
48
|
+
severity: 'critical',
|
|
49
|
+
code: 'template-pack-invalid',
|
|
50
|
+
message: `Template fidelity cannot run because ${error.message}`,
|
|
51
|
+
}],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!pack) {
|
|
58
|
+
return {
|
|
59
|
+
status: 'not-applicable',
|
|
60
|
+
slides: [],
|
|
61
|
+
findings: [],
|
|
62
|
+
referencePreviewFiles: [],
|
|
63
|
+
markdown: '',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const slides = [];
|
|
68
|
+
const findings = [];
|
|
69
|
+
const referencePreviewFiles = [];
|
|
70
|
+
const layouts = Array.isArray(pack.layouts) ? pack.layouts : [];
|
|
71
|
+
|
|
72
|
+
for (const slideFile of slideFiles) {
|
|
73
|
+
const html = await readFile(join(slidesDir, slideFile), 'utf-8');
|
|
74
|
+
const metadata = parseTemplateMetadata(html);
|
|
75
|
+
const generatedPreview = join(previewRelativeDir, slideFile.replace(/\.html$/i, '.png'));
|
|
76
|
+
const slideEntry = {
|
|
77
|
+
slide: slideFile,
|
|
78
|
+
layoutId: metadata?.layoutId ?? null,
|
|
79
|
+
layoutKind: metadata?.layoutKind ?? null,
|
|
80
|
+
generatedPreview,
|
|
81
|
+
referencePreview: '',
|
|
82
|
+
notes: [],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (!metadata?.layoutId) {
|
|
86
|
+
slideEntry.notes.push('not template-governed: no slides-grab-template metadata');
|
|
87
|
+
slides.push(slideEntry);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const layout = layouts.find((candidate) => candidate?.layoutId === metadata.layoutId);
|
|
92
|
+
if (!layout) {
|
|
93
|
+
findings.push({
|
|
94
|
+
severity: 'critical',
|
|
95
|
+
code: 'template-layout-missing',
|
|
96
|
+
slide: slideFile,
|
|
97
|
+
layoutId: metadata.layoutId,
|
|
98
|
+
message: `${slideFile} references missing template layout ${metadata.layoutId}.`,
|
|
99
|
+
});
|
|
100
|
+
slides.push(slideEntry);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
slideEntry.layoutKind = slideEntry.layoutKind ?? layout.layoutKind ?? null;
|
|
105
|
+
slideEntry.referencePreview = layout.preview || '';
|
|
106
|
+
if (!slideEntry.referencePreview) {
|
|
107
|
+
findings.push({
|
|
108
|
+
severity: 'critical',
|
|
109
|
+
code: 'template-reference-preview-missing',
|
|
110
|
+
slide: slideFile,
|
|
111
|
+
layoutId: layout.layoutId,
|
|
112
|
+
message: `${slideFile} selected template layout ${layout.layoutId}, but that layout has no reference preview.`,
|
|
113
|
+
});
|
|
114
|
+
} else if (!existsSync(join(slidesDir, slideEntry.referencePreview))) {
|
|
115
|
+
findings.push({
|
|
116
|
+
severity: 'critical',
|
|
117
|
+
code: 'template-reference-preview-missing',
|
|
118
|
+
slide: slideFile,
|
|
119
|
+
layoutId: layout.layoutId,
|
|
120
|
+
message: `${slideFile} selected template layout ${layout.layoutId}, but reference preview ${slideEntry.referencePreview} is missing.`,
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
referencePreviewFiles.push(slideEntry.referencePreview);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!existsSync(join(slidesDir, generatedPreview))) {
|
|
127
|
+
findings.push({
|
|
128
|
+
severity: 'critical',
|
|
129
|
+
code: 'template-generated-preview-missing',
|
|
130
|
+
slide: slideFile,
|
|
131
|
+
layoutId: layout.layoutId,
|
|
132
|
+
message: `${slideFile} generated preview ${generatedPreview} is missing.`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
slideEntry.notes.push(...buildDeviationNotes({ html, layout, pack }));
|
|
137
|
+
slides.push(slideEntry);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return buildReport({ slides, findings, referencePreviewFiles });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function assertTemplateFidelityPass(report) {
|
|
144
|
+
if (!report || report.status !== 'failed') return report;
|
|
145
|
+
const details = report.findings
|
|
146
|
+
.filter((finding) => finding.severity === 'critical')
|
|
147
|
+
.map((finding) => `${finding.code}: ${finding.message}`)
|
|
148
|
+
.join(' ');
|
|
149
|
+
throw new Error(`Template fidelity blocked proceed: ${details || 'critical template fidelity finding'}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function buildTemplateFidelityMarkdown(report) {
|
|
153
|
+
if (!report || report.status === 'not-applicable') return '';
|
|
154
|
+
const lines = [
|
|
155
|
+
'## Template Fidelity Report',
|
|
156
|
+
'',
|
|
157
|
+
`Status: ${report.status}`,
|
|
158
|
+
'',
|
|
159
|
+
'| Slide | Template layout | Generated preview | Reference preview | Notes |',
|
|
160
|
+
'|---|---|---|---|---|',
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
for (const slide of report.slides) {
|
|
164
|
+
lines.push(`| ${slide.slide} | ${slide.layoutId || 'missing'} | ${slide.generatedPreview || 'missing'} | ${slide.referencePreview || 'missing'} | ${formatNotes(slide.notes)} |`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (report.findings.length > 0) {
|
|
168
|
+
lines.push('', '### Findings', '');
|
|
169
|
+
for (const finding of report.findings) {
|
|
170
|
+
lines.push(`- ${finding.severity.toUpperCase()} ${finding.code}${finding.slide ? ` (${finding.slide})` : ''}: ${finding.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
lines.push('');
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildFailedReport({ findings }) {
|
|
179
|
+
return buildReport({ slides: [], findings, referencePreviewFiles: [] });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildReport({ slides = [], findings = [], referencePreviewFiles = [] }) {
|
|
183
|
+
const status = findings.some((finding) => finding.severity === 'critical') ? 'failed' : 'passed';
|
|
184
|
+
const report = {
|
|
185
|
+
status,
|
|
186
|
+
slides,
|
|
187
|
+
findings,
|
|
188
|
+
referencePreviewFiles: Array.from(new Set(referencePreviewFiles)),
|
|
189
|
+
};
|
|
190
|
+
report.markdown = buildTemplateFidelityMarkdown(report);
|
|
191
|
+
return report;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildDeviationNotes({ html, layout, pack }) {
|
|
195
|
+
const notes = [];
|
|
196
|
+
const expectedColors = Array.isArray(pack.designTokens?.colors)
|
|
197
|
+
? pack.designTokens.colors.map((entry) => normalizeColor(entry.value)).filter(Boolean)
|
|
198
|
+
: [];
|
|
199
|
+
const actualColors = extractColors(html);
|
|
200
|
+
const missingColors = expectedColors.filter((color) => !actualColors.includes(color));
|
|
201
|
+
if (missingColors.length > 0) {
|
|
202
|
+
notes.push(`palette deviation: missing ${missingColors.slice(0, 3).join(', ')}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const expectedFonts = Array.isArray(pack.designTokens?.fonts)
|
|
206
|
+
? pack.designTokens.fonts.map((entry) => normalizeFont(entry.family)).filter(Boolean)
|
|
207
|
+
: [];
|
|
208
|
+
const actualFonts = extractFonts(html);
|
|
209
|
+
const missingFonts = expectedFonts.filter((font) => !actualFonts.includes(font));
|
|
210
|
+
if (missingFonts.length > 0) {
|
|
211
|
+
notes.push(`font deviation: missing ${missingFonts.slice(0, 3).join(', ')}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fieldRoles = Array.isArray(layout.fields)
|
|
215
|
+
? layout.fields.map((field) => field.role).filter(Boolean)
|
|
216
|
+
: [];
|
|
217
|
+
const missingRoles = fieldRoles.filter((role) => !hasTemplateRole(html, role));
|
|
218
|
+
if (missingRoles.length > 0) {
|
|
219
|
+
notes.push(`layout schema roles not explicitly marked: ${missingRoles.slice(0, 3).join(', ')}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (notes.length === 0) notes.push('no lightweight palette/font/layout drift detected');
|
|
223
|
+
return notes;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractColors(html) {
|
|
227
|
+
return Array.from(new Set((html.match(COLOR_RE) || []).map(normalizeColor).filter(Boolean)));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeColor(value) {
|
|
231
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
232
|
+
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(raw);
|
|
233
|
+
if (!match) return null;
|
|
234
|
+
let color = match[1].toLowerCase();
|
|
235
|
+
if (color.length === 3) color = color.split('').map((char) => char + char).join('');
|
|
236
|
+
if (color.length === 8) color = color.slice(0, 6);
|
|
237
|
+
return `#${color}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractFonts(html) {
|
|
241
|
+
const fonts = [];
|
|
242
|
+
FONT_RE.lastIndex = 0;
|
|
243
|
+
let match;
|
|
244
|
+
while ((match = FONT_RE.exec(html)) !== null) {
|
|
245
|
+
const font = normalizeFont(match[1].split(',')[0]);
|
|
246
|
+
if (font && !fonts.includes(font)) fonts.push(font);
|
|
247
|
+
}
|
|
248
|
+
return fonts;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function normalizeFont(value) {
|
|
252
|
+
return String(value || '').trim().replace(/^['"]|['"]$/g, '').toLowerCase();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function hasTemplateRole(html, role) {
|
|
256
|
+
const escaped = escapeRegExp(role);
|
|
257
|
+
return new RegExp(`data-template-role=["']${escaped}["']`, 'i').test(html)
|
|
258
|
+
|| new RegExp(`class=["'][^"']*\\b${escaped}\\b`, 'i').test(html);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function formatNotes(notes) {
|
|
262
|
+
return Array.isArray(notes) && notes.length > 0 ? notes.join('<br>') : 'none';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function escapeRegExp(value) {
|
|
266
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
267
|
+
}
|