@markdstage/markdstage 0.1.1

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 (67) hide show
  1. package/README.md +90 -0
  2. package/bin/markdstage.mjs +12 -0
  3. package/package.json +45 -0
  4. package/shared/README.md +1014 -0
  5. package/shared/THIRD-PARTY-NOTICES.md +19 -0
  6. package/shared/deck-state.mjs +105 -0
  7. package/shared/docs/custom-theme-authoring.md +208 -0
  8. package/shared/markdown-deck.mjs +220 -0
  9. package/shared/markdstage-guide.mjs +276 -0
  10. package/shared/presenter-window.mjs +17 -0
  11. package/shared/renderer/architecture-document.mjs +596 -0
  12. package/shared/renderer/architecture-edit.mjs +298 -0
  13. package/shared/renderer/architecture-editor.mjs +449 -0
  14. package/shared/renderer/architecture.mjs +4033 -0
  15. package/shared/renderer/import-path.mjs +11 -0
  16. package/shared/renderer/index.html +106 -0
  17. package/shared/renderer/renderer.js +2082 -0
  18. package/shared/renderer/slides.css +614 -0
  19. package/shared/renderer/speaker-notes.mjs +106 -0
  20. package/shared/renderer/theme.mjs +205 -0
  21. package/shared/runtime/browser.mjs +539 -0
  22. package/shared/runtime/custom-theme.mjs +135 -0
  23. package/shared/runtime/deck-session.mjs +188 -0
  24. package/shared/runtime/errors.mjs +17 -0
  25. package/shared/runtime/output-paths.mjs +159 -0
  26. package/shared/runtime/output.mjs +385 -0
  27. package/shared/runtime/presentation-server.mjs +505 -0
  28. package/shared/runtime/static-files.mjs +70 -0
  29. package/shared/schema/README.md +228 -0
  30. package/shared/schema/architecture-v1.schema.json +664 -0
  31. package/shared/schema/examples/web-app.architecture.json +119 -0
  32. package/shared/schema/theme-metadata-v1.schema.json +75 -0
  33. package/shared/schema/theme-v1.json +84 -0
  34. package/shared/scripts/architecture-assets.mjs +226 -0
  35. package/shared/scripts/asset-paths.mjs +92 -0
  36. package/shared/scripts/atomic-markdown-replace.mjs +46 -0
  37. package/shared/scripts/markdown-blocks.mjs +182 -0
  38. package/shared/scripts/markdown-files.mjs +63 -0
  39. package/shared/scripts/markdown-save-coordinator.mjs +18 -0
  40. package/shared/scripts/markdown-watcher.mjs +80 -0
  41. package/shared/scripts/theme-paths.mjs +108 -0
  42. package/shared/scripts/vendor-assets.mjs +132 -0
  43. package/shared/scripts/workspace-root.mjs +32 -0
  44. package/shared/vendor/highlight.LICENSE +29 -0
  45. package/shared/vendor/highlight.min.js +1244 -0
  46. package/shared/vendor/marked.min.js +6 -0
  47. package/shared/vendor/mermaid.min.js.part-0001 +268 -0
  48. package/shared/vendor/mermaid.min.js.part-0002 +304 -0
  49. package/shared/vendor/mermaid.min.js.part-0003 +324 -0
  50. package/shared/vendor/mermaid.min.js.part-0004 +374 -0
  51. package/shared/vendor/mermaid.min.js.part-0005 +564 -0
  52. package/shared/vendor/mermaid.min.js.part-0006 +1308 -0
  53. package/shared/vendor/mermaid.min.js.part-0007 +269 -0
  54. package/shared/vendor/purify.min.js +3 -0
  55. package/shared/vendor/vendor-assets.lock.json +60 -0
  56. package/src/cli.mjs +347 -0
  57. package/src/commands/capture.mjs +23 -0
  58. package/src/commands/export.mjs +18 -0
  59. package/src/commands/guide.mjs +23 -0
  60. package/src/commands/inspect.mjs +35 -0
  61. package/src/commands/present.mjs +91 -0
  62. package/src/commands/skill.mjs +114 -0
  63. package/src/commands/validate.mjs +79 -0
  64. package/src/deck.mjs +63 -0
  65. package/src/exit.mjs +58 -0
  66. package/src/runtime.mjs +77 -0
  67. package/src/skills.mjs +155 -0
@@ -0,0 +1,106 @@
1
+ const FENCE_OPEN = /^([ \t]{0,3})(`{3,}|~{3,})[ \t]*([^\s`~]*)[ \t]*$/;
2
+ const SLIDE_SIZE_DIRECTIVE = /^slide-size[ \t]*:/i;
3
+
4
+ function normalizeText(text) {
5
+ return String(text ?? "").replace(/\r\n?/g, "\n");
6
+ }
7
+
8
+ function normalizeNote(text) {
9
+ const lines = normalizeText(text).split("\n");
10
+ while (lines.length && lines[0].trim() === "") lines.shift();
11
+ while (lines.length && lines.at(-1).trim() === "") lines.pop();
12
+ if (!lines.length) return "";
13
+
14
+ const indents = lines
15
+ .filter((line) => line.trim())
16
+ .map((line) => line.match(/^[ \t]*/)?.[0].length ?? 0);
17
+ const indent = indents.length ? Math.min(...indents) : 0;
18
+ return lines.map((line) => line.slice(indent)).join("\n").trim();
19
+ }
20
+
21
+ function closesFence(line, fence) {
22
+ const indentation = line.match(/^[ \t]*/)?.[0].length ?? 0;
23
+ if (indentation > 3) return false;
24
+ const trimmed = line.slice(indentation).trimEnd();
25
+ if (!trimmed || trimmed[0] !== fence[0]) return false;
26
+ let count = 0;
27
+ while (trimmed[count] === fence[0]) count += 1;
28
+ return count >= fence.length && trimmed.slice(count).trim() === "";
29
+ }
30
+
31
+ function parseSpeakerNotes(markdown) {
32
+ const notes = [];
33
+ const output = [];
34
+ let fence = "";
35
+ let comment = null;
36
+
37
+ for (const line of normalizeText(markdown).split("\n")) {
38
+ if (comment === null && fence) {
39
+ if (closesFence(line, fence)) fence = "";
40
+ output.push(line);
41
+ continue;
42
+ }
43
+
44
+ if (comment === null) {
45
+ const opening = FENCE_OPEN.exec(line);
46
+ if (opening) {
47
+ fence = opening[2];
48
+ output.push(line);
49
+ continue;
50
+ }
51
+ }
52
+
53
+ let visible = "";
54
+ let cursor = 0;
55
+ while (cursor <= line.length) {
56
+ if (comment === null) {
57
+ const start = line.indexOf("<!--", cursor);
58
+ if (start < 0) {
59
+ visible += line.slice(cursor);
60
+ break;
61
+ }
62
+ const before = visible + line.slice(cursor, start);
63
+ if (before.trim() || before.length > 3) {
64
+ visible += line.slice(cursor);
65
+ break;
66
+ }
67
+ visible = before;
68
+ comment = [];
69
+ cursor = start + 4;
70
+ }
71
+
72
+ const end = line.indexOf("-->", cursor);
73
+ if (end < 0) {
74
+ comment.push(line.slice(cursor), "\n");
75
+ break;
76
+ }
77
+
78
+ comment.push(line.slice(cursor, end));
79
+ const note = normalizeNote(comment.join(""));
80
+ if (note && !SLIDE_SIZE_DIRECTIVE.test(note)) notes.push(note);
81
+ comment = null;
82
+ cursor = end + 3;
83
+ }
84
+ output.push(visible);
85
+ }
86
+
87
+ return {
88
+ markdown: output.join("\n"),
89
+ notes: notes.join("\n\n"),
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Extract Slidev/Marp-style speaker notes from top-level HTML comments.
95
+ *
96
+ * Comments inside fenced code examples are ignored. The renderer's
97
+ * `slide-size:` comment is a display directive rather than a speaker note.
98
+ */
99
+ export function extractSpeakerNotes(markdown) {
100
+ return parseSpeakerNotes(markdown).notes;
101
+ }
102
+
103
+ /** Remove speaker-note comments while preserving comments in fenced examples. */
104
+ export function stripSpeakerNotes(markdown) {
105
+ return parseSpeakerNotes(markdown).markdown;
106
+ }
@@ -0,0 +1,205 @@
1
+ export const BUILTIN_THEMES = new Set(["dark", "light", "microsoft"]);
2
+ export const THEMES = new Set([...BUILTIN_THEMES, "custom"]);
3
+ export const DEFAULT_THEME = "dark";
4
+ export const THEME_METADATA_VERSION = 1;
5
+ export const THEME_ASSET_MAX_BYTES = 2 * 1024 * 1024;
6
+
7
+ const THEME_ASSET_SEGMENT = "[A-Za-z0-9][A-Za-z0-9_-]*(?:\\.[A-Za-z0-9_-]+)*";
8
+ const THEME_ASSET_PATTERN = new RegExp(
9
+ `^assets/(?:${THEME_ASSET_SEGMENT}/)*${THEME_ASSET_SEGMENT}\\.(?:svg|png|webp|jpg|jpeg)$`,
10
+ "i",
11
+ );
12
+
13
+ export function normalizeTheme(value) {
14
+ const theme = typeof value === "string" ? value.trim().toLowerCase() : "";
15
+ return THEMES.has(theme) ? theme : DEFAULT_THEME;
16
+ }
17
+
18
+ export function parseFrontMatter(markdown) {
19
+ const meta = {};
20
+ const text = String(markdown ?? "").replace(/\r\n?/g, "\n");
21
+ const trimmed = text.replace(/^[\n \t\uFEFF]+/, "");
22
+ if (!trimmed.startsWith("---\n") && trimmed !== "---") return meta;
23
+ const lines = trimmed.split("\n");
24
+ for (let index = 1; index < lines.length; index += 1) {
25
+ if (lines[index].trim() === "---") break;
26
+ const separator = lines[index].indexOf(":");
27
+ if (separator <= 0) continue;
28
+ const key = lines[index].slice(0, separator).trim();
29
+ const value = lines[index]
30
+ .slice(separator + 1)
31
+ .trim()
32
+ .replace(/^["']+|["']+$/g, "");
33
+ if (key) meta[key] = value;
34
+ }
35
+ return meta;
36
+ }
37
+
38
+ export function resolveFrontMatterTheme(slides) {
39
+ for (const slide of Array.isArray(slides) ? slides : []) {
40
+ const meta = parseFrontMatter(slide);
41
+ if (typeof meta.theme === "string" && meta.theme.trim()) {
42
+ return {
43
+ theme: normalizeTheme(meta.theme),
44
+ themeFile: typeof meta["theme-file"] === "string" ? meta["theme-file"].trim() : "",
45
+ };
46
+ }
47
+ if (typeof meta["theme-file"] === "string" && meta["theme-file"].trim()) {
48
+ return { theme: "custom", themeFile: meta["theme-file"].trim() };
49
+ }
50
+ }
51
+ return { theme: DEFAULT_THEME, themeFile: "" };
52
+ }
53
+
54
+ function stripCssComments(css) {
55
+ return String(css ?? "").replace(/\/\*[\s\S]*?\*\//g, "");
56
+ }
57
+
58
+ export function parseThemeVariables(css) {
59
+ let body = stripCssComments(css).trim();
60
+ if (body.startsWith(":root")) {
61
+ const match = body.match(/^:root\s*\{([\s\S]*)\}\s*$/);
62
+ if (!match) throw new Error("custom theme CSS must contain only a complete :root block");
63
+ body = match[1].trim();
64
+ }
65
+ const variables = {};
66
+ for (const declaration of body.split(";")) {
67
+ const item = declaration.trim();
68
+ if (!item) continue;
69
+ const match = item.match(/^(--[A-Za-z0-9_-]+)\s*:\s*(.+)$/s);
70
+ if (!match) {
71
+ throw new Error("custom theme CSS may contain only --custom-property declarations");
72
+ }
73
+ const value = match[2].trim();
74
+ if (
75
+ !value ||
76
+ /<\/?style\b|@import\b|expression\s*\(|javascript\s*:|url\s*\(/i.test(value)
77
+ ) {
78
+ throw new Error(`custom theme CSS contains an unsafe value for ${match[1]}`);
79
+ }
80
+ variables[match[1]] = value;
81
+ }
82
+ if (Object.keys(variables).length === 0) {
83
+ throw new Error("custom theme CSS must define at least one custom property");
84
+ }
85
+ return variables;
86
+ }
87
+
88
+ export function serializeThemeVariables(variables) {
89
+ return Object.entries(variables)
90
+ .map(([name, value]) => `${name}:${value};`)
91
+ .join("");
92
+ }
93
+
94
+ function assertPlainObject(value, path) {
95
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
96
+ throw new Error(`${path} must be an object`);
97
+ }
98
+ return value;
99
+ }
100
+
101
+ function assertOnlyKeys(value, allowed, path) {
102
+ for (const key of Object.keys(value)) {
103
+ if (!allowed.has(key)) throw new Error(`${path}.${key} is not supported`);
104
+ }
105
+ }
106
+
107
+ function parseImage(value, path, { altRequired = false } = {}) {
108
+ const image = assertPlainObject(value, path);
109
+ assertOnlyKeys(image, new Set(["image", "alt"]), path);
110
+ if (
111
+ typeof image.image !== "string" ||
112
+ image.image.length > 200 ||
113
+ !THEME_ASSET_PATTERN.test(image.image)
114
+ ) {
115
+ throw new Error(
116
+ `${path}.image must be a safe path under the theme assets/ folder using svg, png, webp, jpg, or jpeg`,
117
+ );
118
+ }
119
+ const alt = typeof image.alt === "string" ? image.alt.trim() : "";
120
+ if (altRequired && !alt) throw new Error(`${path}.alt must be a non-empty string`);
121
+ return { image: image.image, ...(alt ? { alt } : {}) };
122
+ }
123
+
124
+ export function parseThemeMetadata(value) {
125
+ const metadata = typeof value === "string" ? JSON.parse(value) : value;
126
+ const root = assertPlainObject(metadata, "theme metadata");
127
+ assertOnlyKeys(root, new Set(["$schema", "version", "cover", "backcover"]), "theme metadata");
128
+ if (root.version !== THEME_METADATA_VERSION) {
129
+ throw new Error(`theme metadata version must be ${THEME_METADATA_VERSION}`);
130
+ }
131
+
132
+ const result = { version: THEME_METADATA_VERSION };
133
+ if (root.cover !== undefined) {
134
+ const cover = assertPlainObject(root.cover, "cover");
135
+ assertOnlyKeys(cover, new Set(["background", "logo"]), "cover");
136
+ result.cover = {};
137
+ if (cover.background !== undefined) {
138
+ result.cover.background = parseImage(cover.background, "cover.background");
139
+ }
140
+ if (cover.logo !== undefined) {
141
+ result.cover.logo = parseImage(cover.logo, "cover.logo", { altRequired: true });
142
+ }
143
+ if (Object.keys(result.cover).length === 0) delete result.cover;
144
+ }
145
+
146
+ if (root.backcover !== undefined) {
147
+ const backcover = assertPlainObject(root.backcover, "backcover");
148
+ assertOnlyKeys(backcover, new Set(["logo", "copyright"]), "backcover");
149
+ result.backcover = {};
150
+ if (backcover.logo !== undefined) {
151
+ result.backcover.logo = parseImage(backcover.logo, "backcover.logo", {
152
+ altRequired: true,
153
+ });
154
+ }
155
+ if (backcover.copyright !== undefined) {
156
+ if (typeof backcover.copyright !== "string") {
157
+ throw new Error("backcover.copyright must be a string");
158
+ }
159
+ result.backcover.copyright = backcover.copyright;
160
+ }
161
+ if (Object.keys(result.backcover).length === 0) delete result.backcover;
162
+ }
163
+ return result;
164
+ }
165
+
166
+ export function themeMetadataAssetPaths(metadata) {
167
+ const paths = [];
168
+ const add = (entry) => {
169
+ if (entry?.image && !paths.includes(entry.image)) paths.push(entry.image);
170
+ };
171
+ add(metadata?.cover?.background);
172
+ add(metadata?.cover?.logo);
173
+ add(metadata?.backcover?.logo);
174
+ return paths;
175
+ }
176
+
177
+ export function mapThemeMetadataAssets(metadata, mapAsset) {
178
+ const mapImage = (entry) =>
179
+ entry ? { ...entry, image: mapAsset(entry.image) } : undefined;
180
+ return {
181
+ version: metadata.version,
182
+ ...(metadata.cover
183
+ ? {
184
+ cover: {
185
+ ...(metadata.cover.background
186
+ ? { background: mapImage(metadata.cover.background) }
187
+ : {}),
188
+ ...(metadata.cover.logo ? { logo: mapImage(metadata.cover.logo) } : {}),
189
+ },
190
+ }
191
+ : {}),
192
+ ...(metadata.backcover
193
+ ? {
194
+ backcover: {
195
+ ...(metadata.backcover.logo
196
+ ? { logo: mapImage(metadata.backcover.logo) }
197
+ : {}),
198
+ ...("copyright" in metadata.backcover
199
+ ? { copyright: metadata.backcover.copyright }
200
+ : {}),
201
+ },
202
+ }
203
+ : {}),
204
+ };
205
+ }