@airalogy/aimd-renderer 2.4.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.
- package/README.md +117 -0
- package/README.zh-CN.md +101 -0
- package/dist/aimd-renderer.css +1 -0
- package/dist/html.js +11 -0
- package/dist/index.js +439 -0
- package/dist/processor-Cv8E7QsA.js +11539 -0
- package/dist/vue.js +17 -0
- package/package.json +84 -0
- package/src/__tests__/renderer.test.ts +388 -0
- package/src/common/annotateStepReferences.ts +110 -0
- package/src/common/assignerHighlighting.ts +159 -0
- package/src/common/assignerVisibility.ts +289 -0
- package/src/common/eventKeys.ts +10 -0
- package/src/common/figureNumbering.ts +126 -0
- package/src/common/processor.ts +1554 -0
- package/src/common/quiz-preview.ts +22 -0
- package/src/common/unified-token-renderer.ts +810 -0
- package/src/css.d.ts +1 -0
- package/src/html/index.ts +33 -0
- package/src/index.ts +114 -0
- package/src/locales.ts +256 -0
- package/src/styles/katex.css +2 -0
- package/src/vue/index.ts +47 -0
- package/src/vue/vue-renderer.ts +1449 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { Element, Root as HastRoot, Text as HastText } from "hast"
|
|
2
|
+
import type { ShikiHighlighter } from "../vue/vue-renderer"
|
|
3
|
+
import type { AimdRendererOptions } from "./processor"
|
|
4
|
+
import { buildInlineStyle, resolveAssignerVisibility } from "./assignerVisibility"
|
|
5
|
+
|
|
6
|
+
let assignerHighlighterLoadPromise: Promise<ShikiHighlighter | null> | null = null
|
|
7
|
+
const ASSIGNER_HIGHLIGHT_THEME = "github-light"
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// HAST helpers (also used by the pipeline coordinator)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export function getClassNames(value: unknown): string[] {
|
|
14
|
+
if (Array.isArray(value)) {
|
|
15
|
+
return value.filter((item): item is string => typeof item === "string")
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === "string" && value.trim()) {
|
|
18
|
+
return value.trim().split(/\s+/)
|
|
19
|
+
}
|
|
20
|
+
return []
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hasClassName(node: Element, className: string): boolean {
|
|
24
|
+
return getClassNames(node.properties?.className).includes(className)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getCodeLanguage(node: Element): string | null {
|
|
28
|
+
const languageClass = getClassNames(node.properties?.className).find(name => name.startsWith("language-"))
|
|
29
|
+
return languageClass ? languageClass.replace("language-", "") : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function collectTextContent(node: Element): string {
|
|
33
|
+
return node.children.map((child) => {
|
|
34
|
+
if (child.type === "text") {
|
|
35
|
+
return child.value
|
|
36
|
+
}
|
|
37
|
+
if (child.type === "element") {
|
|
38
|
+
return collectTextContent(child)
|
|
39
|
+
}
|
|
40
|
+
return ""
|
|
41
|
+
}).join("")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function visitHastElements(node: HastRoot | Element, visitor: (element: Element) => void): void {
|
|
45
|
+
if (node.type === "element") {
|
|
46
|
+
visitor(node)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const children = "children" in node ? node.children : []
|
|
50
|
+
for (const child of children) {
|
|
51
|
+
if (child.type === "element") {
|
|
52
|
+
visitHastElements(child, visitor)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function findDescendantElement(node: Element, predicate: (element: Element) => boolean): Element | null {
|
|
58
|
+
for (const child of node.children) {
|
|
59
|
+
if (child.type !== "element") {
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
if (predicate(child)) {
|
|
63
|
+
return child
|
|
64
|
+
}
|
|
65
|
+
const nested = findDescendantElement(child, predicate)
|
|
66
|
+
if (nested) {
|
|
67
|
+
return nested
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Shiki highlighting for assigner code blocks
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function createHighlightedLineChildren(
|
|
78
|
+
tokens: Array<{ content: string, variants: Record<string, { color: string }> }>,
|
|
79
|
+
): Array<Element | HastText> {
|
|
80
|
+
if (tokens.length === 0) {
|
|
81
|
+
return [{ type: "text", value: "\u00A0" }]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return tokens.map((token) => {
|
|
85
|
+
const color = Object.values(token.variants || {}).find(variant => typeof variant?.color === "string")?.color
|
|
86
|
+
return {
|
|
87
|
+
type: "element",
|
|
88
|
+
tagName: "span",
|
|
89
|
+
properties: color ? { style: buildInlineStyle({ color }) } : {},
|
|
90
|
+
children: [{ type: "text", value: token.content || " " }],
|
|
91
|
+
} as Element
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function getAssignerHighlighter(): Promise<ShikiHighlighter | null> {
|
|
96
|
+
if (!assignerHighlighterLoadPromise) {
|
|
97
|
+
assignerHighlighterLoadPromise = import("shiki")
|
|
98
|
+
.then(async ({ createHighlighter }) => {
|
|
99
|
+
const highlighter = await createHighlighter({
|
|
100
|
+
themes: [ASSIGNER_HIGHLIGHT_THEME],
|
|
101
|
+
langs: ["javascript", "python"],
|
|
102
|
+
})
|
|
103
|
+
return highlighter as unknown as ShikiHighlighter
|
|
104
|
+
})
|
|
105
|
+
.catch(() => null)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return assignerHighlighterLoadPromise
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Walk a HAST tree and apply Shiki syntax highlighting to every visible
|
|
113
|
+
* assigner code block (`<code>` inside `.aimd-assigner-preview`).
|
|
114
|
+
*/
|
|
115
|
+
export async function highlightVisibleAssigners(tree: HastRoot, options: AimdRendererOptions): Promise<void> {
|
|
116
|
+
if (resolveAssignerVisibility(options.assignerVisibility) === "hidden") {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const highlighter = await getAssignerHighlighter()
|
|
121
|
+
if (!highlighter?.codeToTokensWithThemes) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
visitHastElements(tree, (element) => {
|
|
126
|
+
if (!hasClassName(element, "aimd-assigner-preview")) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const codeNode = findDescendantElement(element, candidate => candidate.tagName === "code")
|
|
131
|
+
if (!codeNode || codeNode.properties?.["data-aimd-highlighted"] === "true") {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const lang = getCodeLanguage(codeNode) || "text"
|
|
136
|
+
const codeContent = collectTextContent(codeNode)
|
|
137
|
+
const lines = highlighter.codeToTokensWithThemes?.(codeContent, {
|
|
138
|
+
lang,
|
|
139
|
+
themes: { light: ASSIGNER_HIGHLIGHT_THEME },
|
|
140
|
+
}) || []
|
|
141
|
+
|
|
142
|
+
codeNode.children = lines.map((lineTokens) => {
|
|
143
|
+
return {
|
|
144
|
+
type: "element",
|
|
145
|
+
tagName: "span",
|
|
146
|
+
properties: {
|
|
147
|
+
className: ["aimd-assigner-code__line"],
|
|
148
|
+
style: buildInlineStyle({ display: "block" }),
|
|
149
|
+
},
|
|
150
|
+
children: createHighlightedLineChildren(lineTokens),
|
|
151
|
+
} as Element
|
|
152
|
+
})
|
|
153
|
+
codeNode.properties = {
|
|
154
|
+
...codeNode.properties,
|
|
155
|
+
className: [...getClassNames(codeNode.properties?.className), "aimd-assigner-code"],
|
|
156
|
+
"data-aimd-highlighted": "true",
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { Plugin } from "unified"
|
|
2
|
+
import type { AimdRendererOptions } from "./processor"
|
|
3
|
+
import type { AimdAssignerVisibility } from "./processor"
|
|
4
|
+
import { createAimdRendererMessages } from "../locales"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal markdown AST node shape used during remark processing.
|
|
8
|
+
*/
|
|
9
|
+
export interface MarkdownNode {
|
|
10
|
+
type: string
|
|
11
|
+
children?: MarkdownNode[]
|
|
12
|
+
lang?: string | null
|
|
13
|
+
meta?: string | null
|
|
14
|
+
value?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type MarkdownParent = MarkdownNode & { children: MarkdownNode[] }
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export function resolveAssignerVisibility(
|
|
24
|
+
visibility: AimdRendererOptions["assignerVisibility"],
|
|
25
|
+
): AimdAssignerVisibility {
|
|
26
|
+
switch (visibility) {
|
|
27
|
+
case "collapsed":
|
|
28
|
+
case "expanded":
|
|
29
|
+
return visibility
|
|
30
|
+
default:
|
|
31
|
+
return "hidden"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isAssignerCodeNode(node: MarkdownNode): boolean {
|
|
36
|
+
return node.type === "code" && (node.lang || "").trim().toLowerCase() === "assigner"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getAssignerRuntime(meta: string | null | undefined): "client" | "server" {
|
|
40
|
+
const runtime = String((meta || "").match(/\bruntime\s*=\s*([^\s]+)/)?.[1] || "")
|
|
41
|
+
.trim()
|
|
42
|
+
.replace(/^['"]|['"]$/g, "")
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
return runtime === "client" ? "client" : "server"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getRenderedAssignerLanguage(runtime: "client" | "server"): "javascript" | "python" {
|
|
48
|
+
return runtime === "client" ? "javascript" : "python"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function escapeHtml(value: string): string {
|
|
52
|
+
return value
|
|
53
|
+
.replace(/&/g, "&")
|
|
54
|
+
.replace(/</g, "<")
|
|
55
|
+
.replace(/>/g, ">")
|
|
56
|
+
.replace(/"/g, """)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildInlineStyle(declarations: Record<string, string>): string {
|
|
60
|
+
return Object.entries(declarations)
|
|
61
|
+
.map(([property, value]) => `${property}:${value}`)
|
|
62
|
+
.join(";")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Presentation helpers
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
interface AssignerPreviewPresentation {
|
|
70
|
+
badge: string
|
|
71
|
+
containerStyle: string
|
|
72
|
+
headerStyle: string
|
|
73
|
+
titleStyle: string
|
|
74
|
+
badgeStyle: string
|
|
75
|
+
preStyle: string
|
|
76
|
+
codeStyle: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getCollapsedAssignerPresentation(runtime: "client" | "server"): AssignerPreviewPresentation {
|
|
80
|
+
const isClient = runtime === "client"
|
|
81
|
+
const accent = isClient ? "#0f766e" : "#9a3412"
|
|
82
|
+
const accentSoft = isClient ? "rgba(15, 118, 110, 0.08)" : "rgba(154, 52, 18, 0.08)"
|
|
83
|
+
const border = "rgba(148, 163, 184, 0.26)"
|
|
84
|
+
const codeBackground = "#f8fafc"
|
|
85
|
+
const codeForeground = "#0f172a"
|
|
86
|
+
const ruleColor = "rgba(148, 163, 184, 0.18)"
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
badge: isClient ? "JS" : "PY",
|
|
90
|
+
containerStyle: buildInlineStyle({
|
|
91
|
+
margin: "0.85rem 0",
|
|
92
|
+
border: `1px solid ${border}`,
|
|
93
|
+
"border-radius": "12px",
|
|
94
|
+
overflow: "hidden",
|
|
95
|
+
background: "rgba(255, 255, 255, 0.92)",
|
|
96
|
+
}),
|
|
97
|
+
headerStyle: buildInlineStyle({
|
|
98
|
+
display: "flex",
|
|
99
|
+
"align-items": "center",
|
|
100
|
+
"justify-content": "space-between",
|
|
101
|
+
gap: "0.7rem",
|
|
102
|
+
padding: "0.6rem 0.8rem",
|
|
103
|
+
"list-style": "none",
|
|
104
|
+
background: "rgba(248, 250, 252, 0.92)",
|
|
105
|
+
color: "#64748b",
|
|
106
|
+
"font-weight": "600",
|
|
107
|
+
"font-size": "0.86rem",
|
|
108
|
+
}),
|
|
109
|
+
titleStyle: buildInlineStyle({
|
|
110
|
+
display: "inline-flex",
|
|
111
|
+
"align-items": "center",
|
|
112
|
+
gap: "0.45rem",
|
|
113
|
+
"letter-spacing": "0.01em",
|
|
114
|
+
}),
|
|
115
|
+
badgeStyle: buildInlineStyle({
|
|
116
|
+
display: "inline-flex",
|
|
117
|
+
"align-items": "center",
|
|
118
|
+
"justify-content": "center",
|
|
119
|
+
padding: "0.12rem 0.44rem",
|
|
120
|
+
"min-width": "2rem",
|
|
121
|
+
"border-radius": "999px",
|
|
122
|
+
border: `1px solid ${accentSoft}`,
|
|
123
|
+
background: accentSoft,
|
|
124
|
+
color: accent,
|
|
125
|
+
"font-size": "0.72rem",
|
|
126
|
+
"font-weight": "700",
|
|
127
|
+
"letter-spacing": "0.08em",
|
|
128
|
+
}),
|
|
129
|
+
preStyle: buildInlineStyle({
|
|
130
|
+
margin: "0",
|
|
131
|
+
padding: "0.8rem 0.85rem 0.9rem",
|
|
132
|
+
overflow: "auto",
|
|
133
|
+
background: codeBackground,
|
|
134
|
+
border: "0",
|
|
135
|
+
"border-top": `1px solid ${ruleColor}`,
|
|
136
|
+
"tab-size": "2",
|
|
137
|
+
}),
|
|
138
|
+
codeStyle: buildInlineStyle({
|
|
139
|
+
display: "block",
|
|
140
|
+
color: codeForeground,
|
|
141
|
+
background: "transparent",
|
|
142
|
+
"font-family": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace",
|
|
143
|
+
"font-size": "0.88rem",
|
|
144
|
+
"line-height": "1.6",
|
|
145
|
+
"white-space": "pre",
|
|
146
|
+
padding: "0",
|
|
147
|
+
}),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getExpandedAssignerPresentation(runtime: "client" | "server"): AssignerPreviewPresentation {
|
|
152
|
+
const presentation = getCollapsedAssignerPresentation(runtime)
|
|
153
|
+
return {
|
|
154
|
+
...presentation,
|
|
155
|
+
containerStyle: buildInlineStyle({
|
|
156
|
+
...Object.fromEntries(presentation.containerStyle.split(";").filter(Boolean).map(rule => {
|
|
157
|
+
const [property, value] = rule.split(":")
|
|
158
|
+
return [property, value]
|
|
159
|
+
})),
|
|
160
|
+
margin: "1rem 0",
|
|
161
|
+
}),
|
|
162
|
+
headerStyle: buildInlineStyle({
|
|
163
|
+
...Object.fromEntries(presentation.headerStyle.split(";").filter(Boolean).map(rule => {
|
|
164
|
+
const [property, value] = rule.split(":")
|
|
165
|
+
return [property, value]
|
|
166
|
+
})),
|
|
167
|
+
cursor: "default",
|
|
168
|
+
}),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createAssignerHeaderHtml(summary: string, presentation: AssignerPreviewPresentation): string {
|
|
173
|
+
return `<span style="${presentation.titleStyle}">${escapeHtml(summary)}</span>`
|
|
174
|
+
+ `<span aria-hidden="true" style="${presentation.badgeStyle}">${presentation.badge}</span>`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Assigner AST node builders
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function buildExpandedAssignerNode(
|
|
182
|
+
value: string,
|
|
183
|
+
runtime: "client" | "server",
|
|
184
|
+
options: AimdRendererOptions,
|
|
185
|
+
): MarkdownNode {
|
|
186
|
+
const messages = createAimdRendererMessages(options.locale, options.messages)
|
|
187
|
+
const language = getRenderedAssignerLanguage(runtime)
|
|
188
|
+
const summary = runtime === "client"
|
|
189
|
+
? messages.assigner.clientSummary
|
|
190
|
+
: messages.assigner.serverSummary
|
|
191
|
+
const presentation = getExpandedAssignerPresentation(runtime)
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
type: "html",
|
|
195
|
+
value:
|
|
196
|
+
`<div class="aimd-assigner-preview aimd-assigner-preview--expanded aimd-assigner-preview--${runtime}" data-aimd-assigner-runtime="${runtime}" style="${presentation.containerStyle}">`
|
|
197
|
+
+ `<div style="${presentation.headerStyle}">`
|
|
198
|
+
+ createAssignerHeaderHtml(summary, presentation)
|
|
199
|
+
+ "</div>"
|
|
200
|
+
+ `<pre style="${presentation.preStyle}"><code class="language-${language}" style="${presentation.codeStyle}">${escapeHtml(value)}</code></pre>`
|
|
201
|
+
+ "</div>",
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function buildCollapsedAssignerNode(
|
|
206
|
+
value: string,
|
|
207
|
+
runtime: "client" | "server",
|
|
208
|
+
options: AimdRendererOptions,
|
|
209
|
+
): MarkdownNode {
|
|
210
|
+
const messages = createAimdRendererMessages(options.locale, options.messages)
|
|
211
|
+
const language = getRenderedAssignerLanguage(runtime)
|
|
212
|
+
const summary = runtime === "client"
|
|
213
|
+
? messages.assigner.clientSummary
|
|
214
|
+
: messages.assigner.serverSummary
|
|
215
|
+
const presentation = getCollapsedAssignerPresentation(runtime)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
type: "html",
|
|
219
|
+
value:
|
|
220
|
+
`<details class="aimd-assigner-preview aimd-assigner-preview--collapsed aimd-assigner-preview--${runtime}" data-aimd-assigner-runtime="${runtime}" style="${presentation.containerStyle}">`
|
|
221
|
+
+ `<summary style="${presentation.headerStyle}">`
|
|
222
|
+
+ createAssignerHeaderHtml(summary, presentation)
|
|
223
|
+
+ "</summary>"
|
|
224
|
+
+ `<pre style="${presentation.preStyle}"><code class="language-${language}" style="${presentation.codeStyle}">${escapeHtml(value)}</code></pre>`
|
|
225
|
+
+ "</details>",
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Tree visitor
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
export function visitMarkdownParents(node: MarkdownNode, visitor: (parent: MarkdownParent) => void): void {
|
|
234
|
+
if (!Array.isArray(node.children)) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const parent = node as MarkdownParent
|
|
239
|
+
visitor(parent)
|
|
240
|
+
|
|
241
|
+
for (const child of parent.children) {
|
|
242
|
+
visitMarkdownParents(child, visitor)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Remark plugins
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Remark plugin that inserts visible assigner preview nodes (collapsed or
|
|
252
|
+
* expanded) next to each assigner code block in the markdown AST.
|
|
253
|
+
*/
|
|
254
|
+
export const remarkInsertVisibleAssigners: Plugin<[AimdRendererOptions?], MarkdownNode> = (options = {}) => {
|
|
255
|
+
return (tree) => {
|
|
256
|
+
const visibility = resolveAssignerVisibility(options.assignerVisibility)
|
|
257
|
+
if (visibility === "hidden") {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
visitMarkdownParents(tree, (parent) => {
|
|
262
|
+
for (let index = 0; index < parent.children.length; index++) {
|
|
263
|
+
const child = parent.children[index]
|
|
264
|
+
if (!isAssignerCodeNode(child)) {
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const runtime = getAssignerRuntime(child.meta)
|
|
269
|
+
const replacement = visibility === "expanded"
|
|
270
|
+
? buildExpandedAssignerNode(child.value || "", runtime, options)
|
|
271
|
+
: buildCollapsedAssignerNode(child.value || "", runtime, options)
|
|
272
|
+
|
|
273
|
+
parent.children.splice(index, 0, replacement)
|
|
274
|
+
index += 1
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Remark plugin that strips all assigner code blocks from the markdown AST.
|
|
282
|
+
*/
|
|
283
|
+
export const remarkStripAssignerCodeBlocks: Plugin<[], MarkdownNode> = () => {
|
|
284
|
+
return (tree) => {
|
|
285
|
+
visitMarkdownParents(tree, (parent) => {
|
|
286
|
+
parent.children = parent.children.filter(child => !isAssignerCodeNode(child))
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event bus keys for AIMD system
|
|
3
|
+
*/
|
|
4
|
+
import type { InjectionKey } from "vue"
|
|
5
|
+
|
|
6
|
+
export const fieldEventKey: InjectionKey<{ name: "field-event" }> = Symbol("symbol-field-event-key")
|
|
7
|
+
export const protocolKey: InjectionKey<{ name: "protocol-key" }> = Symbol("symbol-protocol-key")
|
|
8
|
+
export const draftEventKey: InjectionKey<{ name: "draft-event" }> = Symbol("draft-event-key")
|
|
9
|
+
export const reportEventKey: InjectionKey<{ name: "report-event" }> = Symbol("symbol-report-event-key")
|
|
10
|
+
export const bubbleMenuEventKey: InjectionKey<{ name: "bubble-menu-event" }> = Symbol("symbol-bubble-menu-event-key")
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Element, Text as HastText, Root as HastRoot } from "hast"
|
|
2
|
+
import type { ExtractedAimdFields } from "@airalogy/aimd-core/types"
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Figure numbering helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a map from figure id to its sequence number based on the
|
|
10
|
+
* extracted fields. The sequence is assigned in document order (1-based).
|
|
11
|
+
*/
|
|
12
|
+
export function buildFigureSequenceMap(fields: ExtractedAimdFields): Map<string, number> {
|
|
13
|
+
const sequenceMap = new Map<string, number>()
|
|
14
|
+
const figs = fields.fig || []
|
|
15
|
+
let seq = 1
|
|
16
|
+
for (const fig of figs) {
|
|
17
|
+
const id = typeof fig === "string" ? fig : (fig as any)?.id
|
|
18
|
+
if (typeof id === "string" && id.trim() && !sequenceMap.has(id)) {
|
|
19
|
+
sequenceMap.set(id, seq++)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return sequenceMap
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the HAST children array for a figure AIMD node.
|
|
27
|
+
*
|
|
28
|
+
* Returns `[imgElement]` or `[imgElement, figcaptionElement]` depending on
|
|
29
|
+
* whether a title or legend is present.
|
|
30
|
+
*/
|
|
31
|
+
export function buildFigureChildren(figNode: {
|
|
32
|
+
id: string
|
|
33
|
+
src?: string
|
|
34
|
+
title?: string
|
|
35
|
+
legend?: string
|
|
36
|
+
sequence?: number | string
|
|
37
|
+
}): (Element | HastText)[] {
|
|
38
|
+
const figSrc = figNode.src || ""
|
|
39
|
+
const figTitle = figNode.title
|
|
40
|
+
const figLegend = figNode.legend
|
|
41
|
+
|
|
42
|
+
const children: (Element | HastText)[] = []
|
|
43
|
+
|
|
44
|
+
// Create img element
|
|
45
|
+
children.push({
|
|
46
|
+
type: "element",
|
|
47
|
+
tagName: "img",
|
|
48
|
+
properties: {
|
|
49
|
+
src: figSrc,
|
|
50
|
+
alt: figTitle || figNode.id,
|
|
51
|
+
className: ["aimd-figure__image"],
|
|
52
|
+
},
|
|
53
|
+
children: [],
|
|
54
|
+
} as Element)
|
|
55
|
+
|
|
56
|
+
// Create figcaption if title or legend exists
|
|
57
|
+
if (figTitle || figLegend) {
|
|
58
|
+
const captionChildren: (Element | HastText)[] = []
|
|
59
|
+
|
|
60
|
+
if (figTitle) {
|
|
61
|
+
captionChildren.push({
|
|
62
|
+
type: "element",
|
|
63
|
+
tagName: "div",
|
|
64
|
+
properties: { className: ["aimd-figure__title"] },
|
|
65
|
+
children: [{ type: "text", value: figTitle }],
|
|
66
|
+
} as Element)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (figLegend) {
|
|
70
|
+
captionChildren.push({
|
|
71
|
+
type: "element",
|
|
72
|
+
tagName: "div",
|
|
73
|
+
properties: { className: ["aimd-figure__legend"] },
|
|
74
|
+
children: [{ type: "text", value: figLegend }],
|
|
75
|
+
} as Element)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
children.push({
|
|
79
|
+
type: "element",
|
|
80
|
+
tagName: "figcaption",
|
|
81
|
+
properties: { className: ["aimd-figure__caption"] },
|
|
82
|
+
children: captionChildren,
|
|
83
|
+
} as Element)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return children
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Walk the HAST tree and stamp each `[data-aimd-type="fig"]` element with
|
|
91
|
+
* its 1-based sequence number as `data-aimd-fig-sequence`.
|
|
92
|
+
*
|
|
93
|
+
* This is a post-processing pass that runs after the initial HAST is built
|
|
94
|
+
* so that figure numbers are consistent with document order regardless of
|
|
95
|
+
* any reordering caused by rehype plugins.
|
|
96
|
+
*/
|
|
97
|
+
export function assignFigureSequenceNumbers(
|
|
98
|
+
tree: HastRoot,
|
|
99
|
+
fields: ExtractedAimdFields,
|
|
100
|
+
): void {
|
|
101
|
+
const sequenceMap = buildFigureSequenceMap(fields)
|
|
102
|
+
if (sequenceMap.size === 0) {
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const visit = (node: HastRoot | Element): void => {
|
|
107
|
+
if (node.type === "element") {
|
|
108
|
+
const el = node as Element
|
|
109
|
+
if (el.properties?.["data-aimd-type"] === "fig") {
|
|
110
|
+
const figId = el.properties["data-aimd-fig-id"] as string | undefined
|
|
111
|
+
if (figId && sequenceMap.has(figId)) {
|
|
112
|
+
el.properties["data-aimd-fig-sequence"] = String(sequenceMap.get(figId))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const children = "children" in node ? node.children : []
|
|
118
|
+
for (const child of children) {
|
|
119
|
+
if (child.type === "element") {
|
|
120
|
+
visit(child)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
visit(tree)
|
|
126
|
+
}
|