@airalogy/aimd-renderer 2.6.0 → 2.8.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 (41) hide show
  1. package/README.md +38 -0
  2. package/README.zh-CN.md +38 -0
  3. package/dist/aimd-renderer.css +1 -1
  4. package/dist/common/assetUrls.d.ts +8 -0
  5. package/dist/common/assetUrls.d.ts.map +1 -0
  6. package/dist/common/citationNumbering.d.ts +11 -0
  7. package/dist/common/citationNumbering.d.ts.map +1 -0
  8. package/dist/common/criticMarkup.d.ts +10 -0
  9. package/dist/common/criticMarkup.d.ts.map +1 -0
  10. package/dist/common/processor.d.ts +2 -0
  11. package/dist/common/processor.d.ts.map +1 -1
  12. package/dist/html/index.d.ts +1 -0
  13. package/dist/html/index.d.ts.map +1 -1
  14. package/dist/html.js +1 -1
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +80 -73
  18. package/dist/locales.d.ts +3 -0
  19. package/dist/locales.d.ts.map +1 -1
  20. package/dist/{processor-CHbNEcN8.js → processor-C_zN3-hL.js} +3454 -2534
  21. package/dist/readonly-record-renderer-G9xOkOFe.js +711 -0
  22. package/dist/vue/index.d.ts +2 -0
  23. package/dist/vue/index.d.ts.map +1 -1
  24. package/dist/vue/readonly-record-renderer.d.ts +42 -0
  25. package/dist/vue/readonly-record-renderer.d.ts.map +1 -0
  26. package/dist/vue/vue-renderer.d.ts +7 -1
  27. package/dist/vue/vue-renderer.d.ts.map +1 -1
  28. package/dist/vue.js +20 -13
  29. package/package.json +2 -2
  30. package/src/__tests__/renderer.test.ts +536 -1
  31. package/src/common/assetUrls.ts +22 -0
  32. package/src/common/citationNumbering.ts +265 -0
  33. package/src/common/criticMarkup.ts +97 -0
  34. package/src/common/processor.ts +206 -25
  35. package/src/html/index.ts +4 -0
  36. package/src/index.ts +23 -0
  37. package/src/locales.ts +9 -0
  38. package/src/styles/katex.css +213 -0
  39. package/src/vue/index.ts +22 -0
  40. package/src/vue/readonly-record-renderer.ts +747 -0
  41. package/src/vue/vue-renderer.ts +183 -9
@@ -0,0 +1,265 @@
1
+ import type { Element, Root as HastRoot, RootContent, Text as HastText } from "hast"
2
+ import type { AimdNode, AimdReferenceEntry, ExtractedAimdFields } from "@airalogy/aimd-core/types"
3
+
4
+ export interface AimdCitationReferenceDisplay {
5
+ id: string
6
+ label: string
7
+ summary: string
8
+ }
9
+
10
+ type AimdCitationNodeWithDisplay = AimdNode & {
11
+ refs?: string[]
12
+ citationRefs?: AimdCitationReferenceDisplay[]
13
+ }
14
+
15
+ function isElementNode(node: RootContent | HastRoot): node is Element {
16
+ return node.type === "element"
17
+ }
18
+
19
+ function createTextNode(value: string): HastText {
20
+ return { type: "text", value }
21
+ }
22
+
23
+ function readStringProperty(properties: Record<string, unknown>, keys: string[]): string | undefined {
24
+ for (const key of keys) {
25
+ const value = properties[key]
26
+ if (typeof value === "string") {
27
+ return value
28
+ }
29
+ }
30
+ return undefined
31
+ }
32
+
33
+ function splitCsv(value: string | undefined): string[] {
34
+ return (value || "")
35
+ .split(",")
36
+ .map(part => part.trim())
37
+ .filter(Boolean)
38
+ }
39
+
40
+ function hasClass(element: Element, className: string): boolean {
41
+ const value = element.properties?.className ?? element.properties?.class
42
+ if (Array.isArray(value)) {
43
+ return value.includes(className)
44
+ }
45
+ if (typeof value === "string") {
46
+ return value.split(/\s+/).includes(className)
47
+ }
48
+ return false
49
+ }
50
+
51
+ function isReferenceSectionElement(element: Element): boolean {
52
+ const properties = element.properties || {}
53
+ const aimdType = properties["data-aimd-type"] || properties.dataAimdType
54
+ const aimdData = (element.data as { aimd?: AimdNode } | undefined)?.aimd
55
+ return aimdType === "refs" || aimdData?.fieldType === "refs" || hasClass(element, "aimd-refs")
56
+ }
57
+
58
+ function isCitationElement(element: Element): boolean {
59
+ const properties = element.properties || {}
60
+ const aimdType = properties["data-aimd-type"] || properties.dataAimdType
61
+ const aimdData = (element.data as { aimd?: AimdNode } | undefined)?.aimd
62
+ return aimdType === "cite" || aimdData?.fieldType === "cite" || hasClass(element, "aimd-cite")
63
+ }
64
+
65
+ function getReferenceContainer(entry: AimdReferenceEntry): string | undefined {
66
+ return entry.journal || entry.booktitle || entry.publisher
67
+ }
68
+
69
+ export function formatCitationReferenceSummary(entry: AimdReferenceEntry | undefined, fallbackId: string): string {
70
+ if (!entry) {
71
+ return fallbackId
72
+ }
73
+
74
+ const parts = [
75
+ entry.author,
76
+ entry.year ? `(${entry.year})` : undefined,
77
+ entry.title,
78
+ getReferenceContainer(entry),
79
+ entry.doi ? `doi:${entry.doi}` : undefined,
80
+ entry.url,
81
+ ]
82
+ .map(part => part?.trim())
83
+ .filter((part): part is string => Boolean(part))
84
+
85
+ return parts.length > 0 ? parts.join(" ") : fallbackId
86
+ }
87
+
88
+ function getCitationRefIds(element: Element): string[] {
89
+ const aimdData = (element.data as { aimd?: AimdCitationNodeWithDisplay } | undefined)?.aimd
90
+ if (aimdData?.fieldType === "cite" && Array.isArray(aimdData.refs)) {
91
+ return aimdData.refs.map(ref => String(ref).trim()).filter(Boolean)
92
+ }
93
+
94
+ const properties = element.properties || {}
95
+ const refs = readStringProperty(properties as Record<string, unknown>, ["data-aimd-refs", "dataAimdRefs"])
96
+ const parsedRefs = splitCsv(refs)
97
+ if (parsedRefs.length > 0) {
98
+ return parsedRefs
99
+ }
100
+
101
+ const id = readStringProperty(properties as Record<string, unknown>, ["data-aimd-id", "dataAimdId"])
102
+ return id ? [id] : []
103
+ }
104
+
105
+ function buildCitationLabelMap(fields: ExtractedAimdFields): Map<string, string> {
106
+ const labels = new Map<string, string>()
107
+ for (const [index, entry] of (fields.refs || []).entries()) {
108
+ const id = entry.id?.trim()
109
+ if (id && !labels.has(id)) {
110
+ labels.set(id, String(index + 1))
111
+ }
112
+ }
113
+ return labels
114
+ }
115
+
116
+ function buildCitationSummaryMap(fields: ExtractedAimdFields): Map<string, string> {
117
+ const summaries = new Map<string, string>()
118
+ for (const entry of fields.refs || []) {
119
+ const id = entry.id?.trim()
120
+ if (id && !summaries.has(id)) {
121
+ summaries.set(id, formatCitationReferenceSummary(entry, id))
122
+ }
123
+ }
124
+ return summaries
125
+ }
126
+
127
+ function buildCitationRefs(
128
+ refIds: string[],
129
+ labelByRefId: Map<string, string>,
130
+ summaryByRefId: Map<string, string>,
131
+ ): AimdCitationReferenceDisplay[] {
132
+ return refIds.map(id => ({
133
+ id,
134
+ label: labelByRefId.get(id) || id,
135
+ summary: summaryByRefId.get(id) || id,
136
+ }))
137
+ }
138
+
139
+ function createCitationReferenceElement(citationRef: AimdCitationReferenceDisplay): Element {
140
+ return {
141
+ type: "element",
142
+ tagName: "span",
143
+ properties: {
144
+ className: ["aimd-cite__ref"],
145
+ role: "doc-noteref",
146
+ tabIndex: 0,
147
+ title: citationRef.summary,
148
+ "data-aimd-ref-id": citationRef.id,
149
+ "data-aimd-ref-summary": citationRef.summary,
150
+ "aria-label": citationRef.label === citationRef.id
151
+ ? `Reference ${citationRef.summary}`
152
+ : `Reference ${citationRef.label}: ${citationRef.summary}`,
153
+ },
154
+ children: [
155
+ {
156
+ type: "element",
157
+ tagName: "span",
158
+ properties: { className: ["aimd-cite__label"] },
159
+ children: [createTextNode(citationRef.label)],
160
+ } as Element,
161
+ {
162
+ type: "element",
163
+ tagName: "span",
164
+ properties: {
165
+ className: ["aimd-cite__popover"],
166
+ role: "tooltip",
167
+ },
168
+ children: [createTextNode(citationRef.summary)],
169
+ } as Element,
170
+ ],
171
+ } as Element
172
+ }
173
+
174
+ function createCitationChildren(citationRefs: AimdCitationReferenceDisplay[]): Array<Element | HastText> {
175
+ const children: Array<Element | HastText> = [createTextNode("[")]
176
+
177
+ citationRefs.forEach((citationRef, index) => {
178
+ if (index > 0) {
179
+ children.push(createTextNode(", "))
180
+ }
181
+
182
+ children.push(createCitationReferenceElement(citationRef))
183
+ })
184
+
185
+ children.push(createTextNode("]"))
186
+ return children
187
+ }
188
+
189
+ function annotateCitationElement(element: Element, citationRefs: AimdCitationReferenceDisplay[]): void {
190
+ const properties = element.properties || {}
191
+ element.properties = {
192
+ ...properties,
193
+ "data-aimd-citation-labels": citationRefs.map(ref => ref.label).join(","),
194
+ "data-aimd-citation-summaries": JSON.stringify(citationRefs.map(ref => ref.summary)),
195
+ }
196
+
197
+ const data = ((element as any).data || {}) as { aimd?: AimdCitationNodeWithDisplay }
198
+ if (data.aimd?.fieldType === "cite") {
199
+ data.aimd.citationRefs = citationRefs
200
+ data.aimd.refs = citationRefs.map(ref => ref.id)
201
+ ;(element as any).data = data
202
+ }
203
+
204
+ element.children = createCitationChildren(citationRefs)
205
+ }
206
+
207
+ function walk(node: HastRoot | RootContent, visitor: (element: Element) => void): void {
208
+ if (!isElementNode(node) && node.type !== "root") {
209
+ return
210
+ }
211
+
212
+ if (isElementNode(node)) {
213
+ visitor(node)
214
+ }
215
+
216
+ const children = node.children || []
217
+ for (const child of children) {
218
+ walk(child, visitor)
219
+ }
220
+ }
221
+
222
+ function extractReferenceSections(children: RootContent[], sections: Element[]): RootContent[] {
223
+ const nextChildren: RootContent[] = []
224
+
225
+ for (const child of children) {
226
+ if (isElementNode(child)) {
227
+ if (isReferenceSectionElement(child)) {
228
+ sections.push(child)
229
+ continue
230
+ }
231
+
232
+ child.children = extractReferenceSections(child.children || [], sections) as Element["children"]
233
+ }
234
+
235
+ nextChildren.push(child)
236
+ }
237
+
238
+ return nextChildren
239
+ }
240
+
241
+ export function moveReferenceSectionsToEnd(tree: HastRoot): void {
242
+ const sections: Element[] = []
243
+ tree.children = extractReferenceSections(tree.children, sections)
244
+ if (sections.length > 0) {
245
+ tree.children.push(...sections)
246
+ }
247
+ }
248
+
249
+ export function annotateCitationReferenceLabels(tree: HastRoot, fields: ExtractedAimdFields): void {
250
+ const labelByRefId = buildCitationLabelMap(fields)
251
+ const summaryByRefId = buildCitationSummaryMap(fields)
252
+
253
+ walk(tree, (element) => {
254
+ if (!isCitationElement(element)) {
255
+ return
256
+ }
257
+
258
+ const refIds = getCitationRefIds(element)
259
+ if (refIds.length === 0) {
260
+ return
261
+ }
262
+
263
+ annotateCitationElement(element, buildCitationRefs(refIds, labelByRefId, summaryByRefId))
264
+ })
265
+ }
@@ -0,0 +1,97 @@
1
+ import type { Element, Text as HastText } from "hast"
2
+ import type {
3
+ AimdCriticMarkupBaseNode,
4
+ AimdCriticMarkupChild,
5
+ AimdCriticMarkupNode,
6
+ AimdCriticSubstitutionNode,
7
+ } from "@airalogy/aimd-core/types"
8
+
9
+ function createTextHast(value: string): HastText {
10
+ return { type: "text", value }
11
+ }
12
+
13
+ function isCriticMarkupNode(node: AimdCriticMarkupChild): node is AimdCriticMarkupNode {
14
+ return typeof node.type === "string" && node.type.startsWith("critic")
15
+ }
16
+
17
+ function renderInlineChildren(state: any, children: AimdCriticMarkupChild[] = []): Array<Element | HastText> {
18
+ return children.flatMap((child) => {
19
+ if (child.type === "text") {
20
+ return [createTextHast(child.value ?? "")] as Array<Element | HastText>
21
+ }
22
+
23
+ if (isCriticMarkupNode(child)) {
24
+ const handler = criticMarkupHandlers[child.type as keyof typeof criticMarkupHandlers] as
25
+ | ((state: any, node: any) => Element)
26
+ | undefined
27
+ if (handler) {
28
+ return [handler(state, child)]
29
+ }
30
+ }
31
+
32
+ const rendered = typeof state.one === "function" ? state.one(child, undefined) : null
33
+ return rendered ? [rendered as Element | HastText] : []
34
+ })
35
+ }
36
+
37
+ function createCriticElement(
38
+ tagName: string,
39
+ kind: string,
40
+ children: Array<Element | HastText>,
41
+ extraClassNames: string[] = [],
42
+ ): Element {
43
+ return {
44
+ type: "element",
45
+ tagName,
46
+ properties: {
47
+ className: ["aimd-critic", `aimd-critic--${kind}`, ...extraClassNames],
48
+ "data-critic-kind": kind,
49
+ },
50
+ children,
51
+ }
52
+ }
53
+
54
+ export const criticMarkupHandlers = {
55
+ criticAddition(state: any, node: AimdCriticMarkupBaseNode): Element {
56
+ return createCriticElement("ins", "addition", renderInlineChildren(state, node.children))
57
+ },
58
+
59
+ criticDeletion(state: any, node: AimdCriticMarkupBaseNode): Element {
60
+ return createCriticElement("del", "deletion", renderInlineChildren(state, node.children))
61
+ },
62
+
63
+ criticHighlight(state: any, node: AimdCriticMarkupBaseNode): Element {
64
+ return createCriticElement("mark", "highlight", renderInlineChildren(state, node.children))
65
+ },
66
+
67
+ criticComment(_state: any, node: AimdCriticMarkupBaseNode): Element {
68
+ const value = node.value.trim()
69
+ return createCriticElement("span", "comment", [
70
+ {
71
+ type: "element",
72
+ tagName: "span",
73
+ properties: { className: ["aimd-critic__comment-label"] },
74
+ children: [createTextHast("Comment")],
75
+ },
76
+ {
77
+ type: "element",
78
+ tagName: "span",
79
+ properties: { className: ["aimd-critic__comment-body"] },
80
+ children: [createTextHast(value)],
81
+ },
82
+ ], value ? [] : ["aimd-critic--empty-comment"])
83
+ },
84
+
85
+ criticSubstitution(state: any, node: AimdCriticSubstitutionNode): Element {
86
+ return createCriticElement("span", "substitution", [
87
+ createCriticElement("del", "deletion", renderInlineChildren(state, node.oldChildren), ["aimd-critic--substitution-old"]),
88
+ {
89
+ type: "element",
90
+ tagName: "span",
91
+ properties: { className: ["aimd-critic__replacement-arrow"], "aria-hidden": "true" },
92
+ children: [createTextHast("->")],
93
+ },
94
+ createCriticElement("ins", "addition", renderInlineChildren(state, node.newChildren), ["aimd-critic--substitution-new"]),
95
+ ])
96
+ },
97
+ }