@uniweb/press 0.4.15 → 0.4.16
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/package.json +1 -1
- package/src/docx/Link.jsx +18 -18
- package/src/docx/LinkPart.jsx +10 -9
- package/src/docx/classifyHref.js +61 -0
package/package.json
CHANGED
package/src/docx/Link.jsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Link component for document output.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Emits an in-document anchor for a `#fragment` and a destination for
|
|
5
|
+
* everything else. See `classifyHref` for why that is the whole rule.
|
|
5
6
|
*
|
|
6
7
|
* @param {Object} props
|
|
7
8
|
* @param {Object} props.data - Link data: { label, href }
|
|
8
9
|
*/
|
|
10
|
+
import { classifyHref } from './classifyHref.js'
|
|
9
11
|
|
|
10
12
|
export default function Link({ data, ...props }) {
|
|
11
13
|
if (!data) return null
|
|
@@ -14,18 +16,11 @@ export default function Link({ data, ...props }) {
|
|
|
14
16
|
|
|
15
17
|
if (!href) return null
|
|
16
18
|
|
|
17
|
-
const
|
|
19
|
+
const { anchor } = classifyHref(href)
|
|
18
20
|
|
|
19
|
-
if (
|
|
21
|
+
if (anchor !== null) {
|
|
20
22
|
return (
|
|
21
|
-
<a
|
|
22
|
-
data-type="externalHyperlink"
|
|
23
|
-
data-link={href}
|
|
24
|
-
href={href}
|
|
25
|
-
target="_blank"
|
|
26
|
-
rel="noopener noreferrer"
|
|
27
|
-
{...props}
|
|
28
|
-
>
|
|
23
|
+
<a data-type="internalHyperlink" data-anchor={anchor} href={href} {...props}>
|
|
29
24
|
<span data-type="text" data-style="Hyperlink">
|
|
30
25
|
{label || href}
|
|
31
26
|
</span>
|
|
@@ -33,15 +28,20 @@ export default function Link({ data, ...props }) {
|
|
|
33
28
|
)
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
const anchor = href.startsWith('#') ? href.slice(1) : href
|
|
31
|
+
// `target`/`rel` only affect the React preview, never the compiled
|
|
32
|
+
// document, so they stay on the narrower http test they have always
|
|
33
|
+
// used — opening a mailto: or a site path in a new tab is not what
|
|
34
|
+
// this component previously did and is not what this change is about.
|
|
35
|
+
const offSite = href.startsWith('http')
|
|
42
36
|
|
|
43
37
|
return (
|
|
44
|
-
<a
|
|
38
|
+
<a
|
|
39
|
+
data-type="externalHyperlink"
|
|
40
|
+
data-link={href}
|
|
41
|
+
href={href}
|
|
42
|
+
{...(offSite ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
45
|
<span data-type="text" data-style="Hyperlink">
|
|
46
46
|
{label || href}
|
|
47
47
|
</span>
|
package/src/docx/LinkPart.jsx
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* in-document anchor the same way (see below).
|
|
14
14
|
*/
|
|
15
15
|
import TextRun from './TextRun.jsx'
|
|
16
|
+
import { classifyHref } from './classifyHref.js'
|
|
16
17
|
|
|
17
18
|
/** `parts` is absent only for a hand-built part; fall back to the label. */
|
|
18
19
|
export function labelRuns(part) {
|
|
@@ -22,15 +23,15 @@ export function labelRuns(part) {
|
|
|
22
23
|
export default function LinkPart({ part }) {
|
|
23
24
|
const href = part.href || ''
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
// Same rule as the <Link> builder, from the same module rather than a
|
|
27
|
+
// second copy of the condition — the two used to classify the same href
|
|
28
|
+
// differently, which is what let `mailto:` compile to a dead anchor on
|
|
29
|
+
// one path and a working link on the other.
|
|
30
|
+
const { anchor } = classifyHref(href)
|
|
31
|
+
const linkAttrs =
|
|
32
|
+
anchor !== null
|
|
33
|
+
? { 'data-type': 'internalHyperlink', 'data-anchor': anchor }
|
|
34
|
+
: { 'data-type': 'externalHyperlink', 'data-link': href }
|
|
34
35
|
|
|
35
36
|
return (
|
|
36
37
|
<a {...linkAttrs} href={href}>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide what SHAPE a link takes in the compiled document. INTERNAL helper —
|
|
3
|
+
* not in the barrel.
|
|
4
|
+
*
|
|
5
|
+
* The whole rule: **a `#fragment` is an in-document anchor; everything else is
|
|
6
|
+
* a destination, emitted verbatim.**
|
|
7
|
+
*
|
|
8
|
+
* ## Why that is the whole rule
|
|
9
|
+
*
|
|
10
|
+
* A fragment means "within this document" in every hypertext format, so this
|
|
11
|
+
* is the one classification Press can make from the href alone without knowing
|
|
12
|
+
* anything about the caller. Everything else — `https://…`, `mailto:…`,
|
|
13
|
+
* `/about`, `../sibling`, `page:installation` — is a destination Press has no
|
|
14
|
+
* standing to interpret.
|
|
15
|
+
*
|
|
16
|
+
* What this replaced was a guess: `href.startsWith('http')` treated everything
|
|
17
|
+
* else as an in-document anchor, so `mailto:a@b.com` and `/about` compiled to
|
|
18
|
+
* `<w:hyperlink w:anchor="mailto:a@b.com">` — a reference to a bookmark that
|
|
19
|
+
* cannot exist. That does not merely fail to navigate: **an anchor name is not
|
|
20
|
+
* a URL, so the destination is destroyed** and nothing downstream can recover
|
|
21
|
+
* it. Emitting it as a destination keeps it in the file, where a wrong link is
|
|
22
|
+
* at least visible and diagnosable.
|
|
23
|
+
*
|
|
24
|
+
* ## Why a bare name is NOT accepted as an anchor
|
|
25
|
+
*
|
|
26
|
+
* `section-3` and `page.html` are indistinguishable. Any rule that reads a bare
|
|
27
|
+
* token as a bookmark name necessarily turns some page links into dead anchors
|
|
28
|
+
* — which is the bug above. `#` is the disambiguator; that is what fragments
|
|
29
|
+
* are for. An author writes `#section-3`.
|
|
30
|
+
*
|
|
31
|
+
* ## Resolution is NOT Press's job
|
|
32
|
+
*
|
|
33
|
+
* Press does not know what `page:installation` means and must not learn. That
|
|
34
|
+
* is the same boundary the LaTeX inset helpers state for cite keys and xref
|
|
35
|
+
* ids — *"Resolution stays foundation-side … the foundation passes
|
|
36
|
+
* already-resolved keys in; Press's job is the emission shape"* — and the same
|
|
37
|
+
* one `DocumentProvider` states for `basePath` (*"Press itself has no awareness
|
|
38
|
+
* of the host runtime"*). Press declares no `@uniweb/*` dependency, and
|
|
39
|
+
* resolving a framework reference here would mean duplicating a resolver that
|
|
40
|
+
* lives in the runtime, which is how two implementations drift apart.
|
|
41
|
+
*
|
|
42
|
+
* So an unresolved `page:` reference compiles to a link pointing at the literal
|
|
43
|
+
* string. That is intended: it is the caller's unresolved input, preserved
|
|
44
|
+
* rather than swallowed. Callers compiling Uniweb content resolve authored
|
|
45
|
+
* hrefs before handing strings to the builders.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} href
|
|
48
|
+
* @returns {{ anchor: string | null }} `anchor` is the bare bookmark name for
|
|
49
|
+
* an in-document reference, or null when the href is a destination.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyHref(href) {
|
|
52
|
+
if (!href || typeof href !== 'string') return { anchor: null }
|
|
53
|
+
|
|
54
|
+
// The bookmark name carries no '#'. A paragraph's `data-bookmark="x"`
|
|
55
|
+
// becomes `<w:bookmarkStart w:name="x"/>`, and Word matches
|
|
56
|
+
// `<w:hyperlink w:anchor="x">` against that name, so the fragment marker
|
|
57
|
+
// has to come off or the two never meet.
|
|
58
|
+
if (href.startsWith('#')) return { anchor: href.slice(1) }
|
|
59
|
+
|
|
60
|
+
return { anchor: null }
|
|
61
|
+
}
|