@asd20/ui-next 2.30.0 → 2.31.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/CHANGELOG.md +21 -0
- package/README.md +4 -0
- package/components.js +2 -0
- package/helpers.js +8 -0
- package/package.json +2 -1
- package/src/components/atoms/Asd20Messaging/index.vue +6 -1
- package/src/components/atoms/Asd20RichBodyContent/headingContext.js +255 -0
- package/src/components/atoms/Asd20RichBodyContent/index.vue +31 -0
- package/src/components/atoms/Asd20RichBodyContent/messageDocumentRenderer.js +199 -45
- package/src/components/molecules/Asd20Accordion/index.vue +38 -14
- package/src/components/molecules/Asd20BodyAccordion/index.vue +23 -2
- package/src/components/molecules/Asd20PrintContents/index.vue +42 -0
- package/src/components/molecules/Asd20PrintHeader/index.vue +54 -0
- package/src/components/molecules/Asd20PublicFormField/index.vue +149 -33
- package/src/components/molecules/Asd20TableauEmbed/index.vue +1 -0
- package/src/components/organisms/Asd20PublicFormModal/index.vue +9 -0
- package/src/components/templates/Asd20HandbookDocument/index.vue +85 -0
- package/src/components/templates/Asd20HandbookTemplate/index.vue +121 -69
- package/src/design/_document-print.scss +104 -0
- package/src/design/_print.scss +2 -0
- package/src/helpers/documentPrint.js +184 -0
- package/src/helpers/handbookDocument.js +44 -0
- package/src/helpers/publicFormDefinition.js +13 -6
- package/src/helpers/publicationIntroduction.js +53 -0
- package/templates.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [2.31.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.30.1...ui-next-v2.31.0) (2026-10-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* allow intentional handbook detail-link exclusions ([67475cc](https://github.com/academydistrict20/asd20-ui-next/commit/67475ccf3dd0f240746d5be421be21223b9bb4fa))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add contextual publication heading renderer ([9b3aca9](https://github.com/academydistrict20/asd20-ui-next/commit/9b3aca9f440aeb8cb39c4586e772857e24e7abaf))
|
|
14
|
+
* add opt-in document print layer ([541d4e6](https://github.com/academydistrict20/asd20-ui-next/commit/541d4e65db4b75a8ffff9c4df673bc42104e416a))
|
|
15
|
+
* checkpoint authenticated PDF staging and accessibility acceptance ([4319b1e](https://github.com/academydistrict20/asd20-ui-next/commit/4319b1edf132191c0d7d4d22a179182c819848e0))
|
|
16
|
+
|
|
17
|
+
## [2.30.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.30.0...ui-next-v2.30.1) (2026-10-06)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* update styling on student records form ([3c9e73b](https://github.com/academydistrict20/asd20-ui-next/commit/3c9e73b669569aead47346fe52c5df432d1ecbf7))
|
|
23
|
+
|
|
3
24
|
# [2.30.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.29.1...ui-next-v2.30.0) (2026-10-06)
|
|
4
25
|
|
|
5
26
|
|
package/README.md
CHANGED
|
@@ -149,6 +149,10 @@ and runs `npm run verify:release` before publishing.
|
|
|
149
149
|
|
|
150
150
|
## Change Guidance
|
|
151
151
|
|
|
152
|
+
The [PDFService implementation overview](docs/projects/pdf-service/README.md)
|
|
153
|
+
records the shared PDF publishing proposal, Azure findings, contextual heading
|
|
154
|
+
requirements, affected repositories, and staged implementation work.
|
|
155
|
+
|
|
152
156
|
Before changing this package, identify which public entry point is affected and
|
|
153
157
|
which downstream repos consume it. Template, style/token, icon, input model, and
|
|
154
158
|
helper changes are especially likely to require downstream website or app
|
package/components.js
CHANGED
|
@@ -25,6 +25,8 @@ export { default as Asd20Viewport } from './src/components/atoms/Asd20Viewport/i
|
|
|
25
25
|
export { default as Asd20WidgetViewport } from './src/components/atoms/Asd20WidgetViewport/index.vue'
|
|
26
26
|
|
|
27
27
|
// Molecules
|
|
28
|
+
export { default as Asd20PrintHeader } from './src/components/molecules/Asd20PrintHeader/index.vue'
|
|
29
|
+
export { default as Asd20PrintContents } from './src/components/molecules/Asd20PrintContents/index.vue'
|
|
28
30
|
export { default as Asd20Accordion } from './src/components/molecules/Asd20Accordion/index.vue'
|
|
29
31
|
export { default as Asd20ActionMenuItem } from './src/components/molecules/Asd20ActionMenuItem/index.vue'
|
|
30
32
|
export { default as Asd20BlockSchedule } from './src/components/molecules/Asd20BlockSchedule/index.vue'
|
package/helpers.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
export { default as arraySearch } from './src/helpers/arraySearch.js'
|
|
2
|
+
export { buildPublicationIntroduction } from './src/helpers/publicationIntroduction.js'
|
|
3
|
+
export { prepareDocumentPrint, printDocument, installDocumentPrintLifecycle } from './src/helpers/documentPrint.js'
|
|
2
4
|
export { default as buildFileSearchUrl } from './src/helpers/buildFileSearchUrl.js'
|
|
5
|
+
export {
|
|
6
|
+
buildPublicationHeadingModels,
|
|
7
|
+
buildRichBodyRenderModel,
|
|
8
|
+
} from './src/components/atoms/Asd20RichBodyContent/messageDocumentRenderer.js'
|
|
9
|
+
export { createPublicationHeadingPrefix } from './src/components/atoms/Asd20RichBodyContent/headingContext.js'
|
|
3
10
|
export { getCookie, setCookie } from './src/helpers/cookieRecipe.js'
|
|
4
11
|
export { default as expandEvents } from './src/helpers/expandEvents.js'
|
|
5
12
|
export { default as formatEventTime } from './src/helpers/formatEventTime.js'
|
|
@@ -52,3 +59,4 @@ export { default as queryPages } from './src/helpers/queryPages.js'
|
|
|
52
59
|
export { default as queryPeople } from './src/helpers/queryPeople.js'
|
|
53
60
|
export { default as sendEmail } from './src/helpers/sendEmail.js'
|
|
54
61
|
export { default as sortOrgsByLevel } from './src/helpers/sortOrgsByLevel.js'
|
|
62
|
+
export { buildHandbookDocument, documentImageUrls } from './src/helpers/handbookDocument.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asd20/ui-next",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.31.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ASD20 UI component library for Vue 3.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"focus-trap": "^6.9.4",
|
|
42
42
|
"js-cookie": "3.0.8",
|
|
43
43
|
"lodash": "^4.17.23",
|
|
44
|
+
"parse5": "8.0.0",
|
|
44
45
|
"swiper": "12.2.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
@@ -59,12 +59,15 @@
|
|
|
59
59
|
v-html="longDescription"
|
|
60
60
|
/>
|
|
61
61
|
<div
|
|
62
|
-
v-if="bodyContent"
|
|
62
|
+
v-if="bodyContent || bodyJson"
|
|
63
63
|
class="asd20-messaging__body-content"
|
|
64
64
|
>
|
|
65
65
|
<asd20-rich-body-content
|
|
66
66
|
:body-content="bodyContent"
|
|
67
67
|
:body-json="bodyJson"
|
|
68
|
+
:heading-level-offset="headingLevelOffset"
|
|
69
|
+
:heading-id-prefix="headingIdPrefix"
|
|
70
|
+
@headings-change="$emit('headings-change', $event)"
|
|
68
71
|
/>
|
|
69
72
|
</div>
|
|
70
73
|
|
|
@@ -113,6 +116,8 @@ export default {
|
|
|
113
116
|
images: { type: Array, default: () => [] },
|
|
114
117
|
heading: { type: String, default: '' },
|
|
115
118
|
headingTagName: { type: String, default: 'h2' },
|
|
119
|
+
headingLevelOffset: { type: Number, default: 0 },
|
|
120
|
+
headingIdPrefix: { type: String, default: '' },
|
|
116
121
|
shortDescription: { type: String, default: '' },
|
|
117
122
|
longDescription: { type: String, default: '' },
|
|
118
123
|
bodyContent: { type: String, default: '' },
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { defaultTreeAdapter, parseFragment, serialize, serializeOuter } from 'parse5'
|
|
2
|
+
|
|
3
|
+
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'
|
|
4
|
+
const HEADING_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
|
|
5
|
+
|
|
6
|
+
// Each publication provides a complete, immutable plan before descendants render.
|
|
7
|
+
export const publicationHeadingPlanKey = Symbol('asd20-publication-heading-plan')
|
|
8
|
+
export const contextualAccordionItemKey = Symbol('asd20-contextual-accordion-item')
|
|
9
|
+
|
|
10
|
+
export function createPublicationHeadingPrefix(documentId, occurrenceId) {
|
|
11
|
+
const encode = value => Array.from(new TextEncoder().encode(String(value)),
|
|
12
|
+
byte => byte.toString(16).padStart(2, '0')).join('')
|
|
13
|
+
return `pub-${encode(documentId)}-topic-${encode(occurrenceId)}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeHeadingContext(context = {}) {
|
|
17
|
+
const supplied = context.levelOffset === undefined ? 0 : context.levelOffset
|
|
18
|
+
const valid = Number.isInteger(supplied) && supplied >= 0 && supplied <= 5
|
|
19
|
+
const idPrefix = typeof context.idPrefix === 'string' ? context.idPrefix : ''
|
|
20
|
+
return {
|
|
21
|
+
...context,
|
|
22
|
+
levelOffset: valid ? supplied : 0,
|
|
23
|
+
idPrefix,
|
|
24
|
+
active: supplied !== 0 || Boolean(idPrefix),
|
|
25
|
+
invalidOffset: Boolean(context.invalidOffset) || !valid,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function textLabel(node) {
|
|
30
|
+
if (node.nodeName === '#text') return node.value
|
|
31
|
+
if (['script', 'style', 'template'].includes(node.tagName)) return ''
|
|
32
|
+
if (node.tagName === 'br') return ' '
|
|
33
|
+
return (node.childNodes || []).map(textLabel).join('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getAttribute(node, name) {
|
|
37
|
+
return node.attrs?.find(attr => attr.name === name)?.value || ''
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setAttribute(node, name, value) {
|
|
41
|
+
const existing = node.attrs.find(attr => attr.name === name)
|
|
42
|
+
if (existing) existing.value = value
|
|
43
|
+
else node.attrs.push({ name, value })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fragmentTarget(href, documentUrl) {
|
|
47
|
+
let hash = href.startsWith('#') ? href : ''
|
|
48
|
+
if (!hash && documentUrl) {
|
|
49
|
+
try {
|
|
50
|
+
const base = new URL(documentUrl)
|
|
51
|
+
const url = new URL(href, base)
|
|
52
|
+
if (url.origin === base.origin && url.pathname === base.pathname &&
|
|
53
|
+
url.search === base.search) hash = url.hash
|
|
54
|
+
} catch (_error) { /* Ordinary links retain the existing URL policy. */ }
|
|
55
|
+
}
|
|
56
|
+
if (!hash || hash === '#') return null
|
|
57
|
+
try { return decodeURIComponent(hash.slice(1)) } catch (_error) { return hash.slice(1) }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createHeadingSession(inputContext = {}) {
|
|
61
|
+
const context = normalizeHeadingContext(inputContext)
|
|
62
|
+
const prefix = context.idPrefix || 'body'
|
|
63
|
+
const entries = []
|
|
64
|
+
const headings = []
|
|
65
|
+
const diagnostics = []
|
|
66
|
+
const references = []
|
|
67
|
+
const headingStack = []
|
|
68
|
+
const accordionHeadings = new Map()
|
|
69
|
+
const report = (code, sourceKey, detail = '') => {
|
|
70
|
+
if (!diagnostics.some(row => row.code === code && row.sourceKey === sourceKey && row.detail === detail)) {
|
|
71
|
+
diagnostics.push({ code, sourceKey, detail })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (context.invalidOffset) report('invalid-heading-offset', prefix)
|
|
75
|
+
if (!context.idPrefix) report('heading-namespace-missing', prefix)
|
|
76
|
+
if (!context.idPlan) report('document-heading-plan-unavailable', prefix)
|
|
77
|
+
|
|
78
|
+
function anchor(sourcePath, sourceId, candidate, kind = 'anchor') {
|
|
79
|
+
const key = `${prefix}:${sourcePath}`
|
|
80
|
+
entries.push({ key, sourcePath, sourceId: sourceId || null, candidate, prefix, kind })
|
|
81
|
+
if (sourceId && /\s/.test(sourceId)) report('invalid-source-anchor', key, sourceId)
|
|
82
|
+
const planned = context.idPlan?.idsBySourceKey[key]
|
|
83
|
+
if (context.idPlan && !planned) report('heading-plan-mismatch', key)
|
|
84
|
+
return { key, id: planned || sourceId || candidate }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function heading({ authoredLevel, label, sourcePath, sourceId = null,
|
|
88
|
+
kind = 'detail', accordionItemId = null }) {
|
|
89
|
+
const { key, id } = anchor(sourcePath, sourceId,
|
|
90
|
+
`${prefix}-${sourcePath}`, 'heading')
|
|
91
|
+
const requestedLevel = authoredLevel + context.levelOffset
|
|
92
|
+
const renderedLevel = Math.min(6, requestedLevel)
|
|
93
|
+
const codes = []
|
|
94
|
+
const add = code => { codes.push(code); report(code, key) }
|
|
95
|
+
if (requestedLevel > 6) add('heading-overflow')
|
|
96
|
+
if (authoredLevel === 1 && context.levelOffset) add('authored-h1-in-composition')
|
|
97
|
+
const normalizedLabel = String(label || '').replace(/\s+/g, ' ').trim()
|
|
98
|
+
if (!normalizedLabel) add('empty-heading-label')
|
|
99
|
+
while (headingStack.length && headingStack.at(-1).requestedLevel >= requestedLevel) headingStack.pop()
|
|
100
|
+
let parent = headingStack.at(-1) || null
|
|
101
|
+
const accordionParent = accordionItemId ? accordionHeadings.get(accordionItemId) : null
|
|
102
|
+
if (kind === 'detail' && accordionParent && !parent?.accordionItemId) parent = accordionParent
|
|
103
|
+
if (kind === 'detail' && accordionParent && requestedLevel <= accordionParent.requestedLevel) {
|
|
104
|
+
add('accordion-body-level-conflict')
|
|
105
|
+
}
|
|
106
|
+
// The containing message/topic title is one level above detail H2.
|
|
107
|
+
const parentLevel = parent?.requestedLevel ?? (1 + context.levelOffset)
|
|
108
|
+
if (requestedLevel > parentLevel + 1) add('heading-level-gap')
|
|
109
|
+
const descriptor = {
|
|
110
|
+
key, id, sourceId, sourcePath, kind, label: normalizedLabel,
|
|
111
|
+
authoredLevel, requestedLevel, renderedLevel,
|
|
112
|
+
overflow: requestedLevel > 6,
|
|
113
|
+
parentHeadingId: parent?.id || null, accordionItemId, aliasIds: [], diagnostics: codes,
|
|
114
|
+
}
|
|
115
|
+
headings.push(descriptor)
|
|
116
|
+
headingStack.push(descriptor)
|
|
117
|
+
if (kind === 'accordion-item') accordionHeadings.set(accordionItemId, descriptor)
|
|
118
|
+
return descriptor
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function scope(render) {
|
|
122
|
+
const previous = [...headingStack]
|
|
123
|
+
try { return render() } finally { headingStack.splice(0, headingStack.length, ...previous) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function href(value, sourcePath) {
|
|
127
|
+
const target = fragmentTarget(value, context.documentUrl || context.idPlan?.documentUrl)
|
|
128
|
+
if (target === null) return value
|
|
129
|
+
const sourceKey = `${prefix}:${sourcePath}`
|
|
130
|
+
references.push({ target, prefix, sourceKey })
|
|
131
|
+
const mapped = context.idPlan?.fragmentsByTopic[prefix]?.[target]
|
|
132
|
+
return mapped ? `#${encodeURIComponent(mapped)}` : value
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function html(value, fieldPath, renderPlaceholder = null, accordionItemId = null) {
|
|
136
|
+
const fragment = parseFragment(
|
|
137
|
+
defaultTreeAdapter.createElement('div', HTML_NAMESPACE, []), value,
|
|
138
|
+
{ onParseError: error => report('legacy-html-parse-recovery', `${prefix}:${fieldPath}`, error.code) }
|
|
139
|
+
)
|
|
140
|
+
let headingOrdinal = 0
|
|
141
|
+
const replacements = new Map()
|
|
142
|
+
const walk = (node, path, depth) => {
|
|
143
|
+
if (node.namespaceURI !== HTML_NAMESPACE) {
|
|
144
|
+
(node.childNodes || []).forEach((child, index) => walk(child, `${path}-${index}`, depth + 1))
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
const sourceId = getAttribute(node, 'id')
|
|
148
|
+
if (HEADING_TAGS.has(node.tagName)) {
|
|
149
|
+
const descriptor = heading({ authoredLevel: Number(node.tagName[1]),
|
|
150
|
+
label: textLabel(node), sourcePath: `html-${fieldPath}-h-${headingOrdinal++}`,
|
|
151
|
+
sourceId: sourceId || null, accordionItemId })
|
|
152
|
+
node.tagName = `h${descriptor.renderedLevel}`
|
|
153
|
+
node.nodeName = node.tagName
|
|
154
|
+
setAttribute(node, 'id', descriptor.id)
|
|
155
|
+
} else if (sourceId) {
|
|
156
|
+
const allocated = anchor(`html-${fieldPath}-element-${path}-id`, sourceId,
|
|
157
|
+
`${prefix}-html-${fieldPath}-element-${path}`)
|
|
158
|
+
setAttribute(node, 'id', allocated.id)
|
|
159
|
+
}
|
|
160
|
+
const name = node.tagName === 'a' ? getAttribute(node, 'name') : ''
|
|
161
|
+
if (name) {
|
|
162
|
+
// Identical id/name attributes represent one browser target, not two.
|
|
163
|
+
const allocated = name === sourceId
|
|
164
|
+
? { id: getAttribute(node, 'id') }
|
|
165
|
+
: anchor(`html-${fieldPath}-element-${path}-name`, name,
|
|
166
|
+
`${prefix}-html-${fieldPath}-element-${path}-name`)
|
|
167
|
+
setAttribute(node, 'name', allocated.id)
|
|
168
|
+
}
|
|
169
|
+
const link = node.tagName === 'a' ? getAttribute(node, 'href') : ''
|
|
170
|
+
if (link) setAttribute(node, 'href', href(link, `html-${fieldPath}-element-${path}-href`))
|
|
171
|
+
const blockType = getAttribute(node, 'data-cc-block')
|
|
172
|
+
if (blockType) {
|
|
173
|
+
const nonempty = (node.childNodes || []).some(child =>
|
|
174
|
+
child.nodeName !== '#text' || child.value.trim())
|
|
175
|
+
if (nonempty) report('nonempty-body-placeholder', `${prefix}:${fieldPath}-${path}`)
|
|
176
|
+
else if (depth === 0 && node.tagName === 'div' && renderPlaceholder) {
|
|
177
|
+
const block = renderPlaceholder(blockType, getAttribute(node, 'data-block-id'), path)
|
|
178
|
+
if (block) replacements.set(node, { ...block, rootId: getAttribute(node, 'id') })
|
|
179
|
+
else report('unresolved-body-placeholder', `${prefix}:${fieldPath}-${path}`)
|
|
180
|
+
} else report('nested-body-placeholder', `${prefix}:${fieldPath}-${path}`)
|
|
181
|
+
}
|
|
182
|
+
(node.childNodes || []).forEach((child, index) => walk(child, `${path}-${index}`, depth + 1))
|
|
183
|
+
}
|
|
184
|
+
fragment.childNodes.forEach((node, index) => walk(node, String(index), 0))
|
|
185
|
+
const nodes = []
|
|
186
|
+
let pendingHtml = ''
|
|
187
|
+
const flush = () => {
|
|
188
|
+
if (pendingHtml) nodes.push({ type: 'html', html: pendingHtml })
|
|
189
|
+
pendingHtml = ''
|
|
190
|
+
}
|
|
191
|
+
for (const node of fragment.childNodes) {
|
|
192
|
+
if (replacements.has(node)) { flush(); nodes.push(replacements.get(node)) }
|
|
193
|
+
else pendingHtml += serializeOuter(node)
|
|
194
|
+
}
|
|
195
|
+
flush()
|
|
196
|
+
return { html: serialize(fragment), nodes }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { context, entries, headings, diagnostics, references, report, anchor, heading, href, html, scope }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function buildHeadingIdPlan(sessions, reservedIds = []) {
|
|
203
|
+
const entries = sessions.flatMap(session => session.entries)
|
|
204
|
+
const sourceCounts = new Map()
|
|
205
|
+
const used = new Set(reservedIds)
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (entry.sourceId) {
|
|
208
|
+
sourceCounts.set(entry.sourceId, (sourceCounts.get(entry.sourceId) || 0) + 1)
|
|
209
|
+
used.add(entry.sourceId)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const idsBySourceKey = Object.create(null)
|
|
213
|
+
const fragmentsByTopic = Object.create(null)
|
|
214
|
+
const diagnostics = []
|
|
215
|
+
const sourceTargets = new Map()
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const preserve = entry.sourceId && sourceCounts.get(entry.sourceId) === 1 && !reservedIds.includes(entry.sourceId)
|
|
218
|
+
let id = preserve ? entry.sourceId : entry.candidate
|
|
219
|
+
if (!preserve) {
|
|
220
|
+
const candidate = id
|
|
221
|
+
let suffix = 2
|
|
222
|
+
while (used.has(id)) id = `${candidate}--${suffix++}`
|
|
223
|
+
}
|
|
224
|
+
used.add(id)
|
|
225
|
+
idsBySourceKey[entry.key] = id
|
|
226
|
+
if (entry.sourceId) {
|
|
227
|
+
const local = fragmentsByTopic[entry.prefix] || (fragmentsByTopic[entry.prefix] = Object.create(null))
|
|
228
|
+
if (Object.hasOwn(local, entry.sourceId)) {
|
|
229
|
+
diagnostics.push({ code: 'ambiguous-anchor', sourceKey: entry.key, detail: entry.sourceId })
|
|
230
|
+
} else local[entry.sourceId] = id
|
|
231
|
+
const targets = sourceTargets.get(entry.sourceId) || []
|
|
232
|
+
targets.push(id)
|
|
233
|
+
sourceTargets.set(entry.sourceId, targets)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const session of sessions) {
|
|
237
|
+
const prefix = session.context.idPrefix || 'body'
|
|
238
|
+
const local = fragmentsByTopic[prefix] || (fragmentsByTopic[prefix] = Object.create(null))
|
|
239
|
+
for (const reference of session.references) {
|
|
240
|
+
if (Object.hasOwn(local, reference.target)) continue
|
|
241
|
+
const targets = sourceTargets.get(reference.target)
|
|
242
|
+
if (targets?.length === 1) local[reference.target] = targets[0]
|
|
243
|
+
else if (targets?.length) diagnostics.push({ code: 'ambiguous-anchor', sourceKey: reference.sourceKey, detail: reference.target })
|
|
244
|
+
else if (reservedIds.includes(reference.target)) local[reference.target] = reference.target
|
|
245
|
+
else diagnostics.push({ code: 'unresolved-anchor', sourceKey: reference.sourceKey, detail: reference.target })
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
for (const local of Object.values(fragmentsByTopic)) Object.freeze(local)
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
documentUrl: sessions[0]?.context.documentUrl || '',
|
|
251
|
+
idsBySourceKey: Object.freeze(idsBySourceKey),
|
|
252
|
+
fragmentsByTopic: Object.freeze(fragmentsByTopic),
|
|
253
|
+
diagnostics: Object.freeze(diagnostics.map(row => Object.freeze(row))),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
@@ -14,11 +14,15 @@
|
|
|
14
14
|
/>
|
|
15
15
|
<asd20-body-accordion
|
|
16
16
|
v-else-if="node.type === 'accordion'"
|
|
17
|
+
:id="node.rootId || undefined"
|
|
17
18
|
:key="`accordion-${node.block.id}-${index}`"
|
|
18
19
|
:items="node.block.items"
|
|
20
|
+
:heading-level-offset="headingLevelOffset"
|
|
21
|
+
:heading-id-prefix="headingIdPrefix"
|
|
19
22
|
/>
|
|
20
23
|
<asd20-tableau-embed
|
|
21
24
|
v-else-if="node.type === 'tableau'"
|
|
25
|
+
:id="node.rootId || undefined"
|
|
22
26
|
:key="`tableau-${node.block.id}-${index}`"
|
|
23
27
|
:src="node.block.src"
|
|
24
28
|
:height="node.block.height"
|
|
@@ -45,17 +49,26 @@ import {
|
|
|
45
49
|
buildRichBodyRenderModel,
|
|
46
50
|
containsBodyBlockPlaceholder,
|
|
47
51
|
} from './messageDocumentRenderer'
|
|
52
|
+
import { publicationHeadingPlanKey } from './headingContext'
|
|
48
53
|
|
|
49
54
|
export default {
|
|
50
55
|
name: 'Asd20RichBodyContent',
|
|
51
56
|
|
|
52
57
|
components: { Asd20BodyAccordion, Asd20TableauEmbed },
|
|
53
58
|
|
|
59
|
+
inject: {
|
|
60
|
+
publicationHeadingPlan: { from: publicationHeadingPlanKey, default: null },
|
|
61
|
+
},
|
|
62
|
+
|
|
54
63
|
props: {
|
|
55
64
|
bodyContent: { type: String, default: '' },
|
|
56
65
|
bodyJson: { type: [String, Object], default: '' },
|
|
66
|
+
headingLevelOffset: { type: Number, default: 0 },
|
|
67
|
+
headingIdPrefix: { type: String, default: '' },
|
|
57
68
|
},
|
|
58
69
|
|
|
70
|
+
emits: ['headings-change'],
|
|
71
|
+
|
|
59
72
|
data: () => ({
|
|
60
73
|
hasMounted: false,
|
|
61
74
|
}),
|
|
@@ -65,9 +78,18 @@ export default {
|
|
|
65
78
|
return buildRichBodyRenderModel({
|
|
66
79
|
bodyContent: this.bodyContent,
|
|
67
80
|
bodyJson: this.bodyJson,
|
|
81
|
+
headingContext: {
|
|
82
|
+
levelOffset: this.headingLevelOffset,
|
|
83
|
+
idPrefix: this.headingIdPrefix,
|
|
84
|
+
idPlan: this.publicationHeadingPlan?.(),
|
|
85
|
+
},
|
|
68
86
|
})
|
|
69
87
|
},
|
|
70
88
|
renderNodes() {
|
|
89
|
+
if (this.renderModel.headingContextApplied) {
|
|
90
|
+
return !this.hasMounted && this.renderModel.status === 'legacy'
|
|
91
|
+
? this.renderModel.initialNodes : this.renderModel.nodes
|
|
92
|
+
}
|
|
71
93
|
const html = this.bodyContent || ''
|
|
72
94
|
// Keep SSR and the initial client render on the same raw HTML markup,
|
|
73
95
|
// then enhance block placeholders after hydration.
|
|
@@ -82,6 +104,15 @@ export default {
|
|
|
82
104
|
},
|
|
83
105
|
},
|
|
84
106
|
|
|
107
|
+
watch: {
|
|
108
|
+
renderModel: {
|
|
109
|
+
immediate: true,
|
|
110
|
+
handler(model) {
|
|
111
|
+
this.$emit('headings-change', { headings: model.headings, diagnostics: model.headingDiagnostics })
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
85
116
|
mounted() {
|
|
86
117
|
this.hasMounted = true
|
|
87
118
|
this.applyListColumnExceptions()
|