Skip to content

Commit

Permalink
Try resolving HyperlinkRef with embeded simple name
Browse files Browse the repository at this point in the history
  • Loading branch information
Trinovantes committed Jun 30, 2024
1 parent 9f1ef91 commit 122d3b8
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 26 deletions.
11 changes: 11 additions & 0 deletions src/Generator/RstGeneratorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,17 @@ export class RstGeneratorState implements SimpleNameResolverProxy {
return url
}

resolveMultipleSimpleNamesToUrl(srcNode: RstNode, simpleNames: Array<SimpleName>): string {
for (const simpleName of simpleNames) {
const url = this._currentParserOutput.simpleNameResolver.resolveSimpleNameToUrl(simpleName)
if (url) {
return url
}
}

throw new RstGeneratorError(this, srcNode, 'Failed to resolveMultipleSimpleNamesToUrl')
}

resolveFootnoteDefLabel(footnoteDef: RstFootnoteDef): string {
const label = this._currentParserOutput.simpleNameResolver.resolveFootnoteDefLabel(footnoteDef)
if (!label) {
Expand Down
68 changes: 44 additions & 24 deletions src/Parser/Resolver/SimpleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ type FootnoteSymNum = Brand<number, 'FootnoteSymNum'> // For determining which s
type DocumentTarget = {
target: string // Either a url or ref (SimpleName)
isAlias: boolean
targetNode?: RstNode // If targets a node inside this document
}

export class SimpleNameResolver {
Expand Down Expand Up @@ -97,14 +96,6 @@ export class SimpleNameResolver {
return normalizeSimpleName(node.label)
}

if (node instanceof RstHyperlinkRef) {
// Explicitly named HyperlinkRef that links elsewhere can be referenced by their label
// e.g. `label <url not same as label>`
if (node.isEmbeded) {
return normalizeSimpleName(node.label)
}
}

if (node instanceof RstHyperlinkTarget) {
if (node.isAnonymous) {
return normalizeSimpleName(`${node.nodeType}-anonymous-${node.nthOfType}`)
Expand Down Expand Up @@ -151,32 +142,31 @@ export class SimpleNameResolver {
}
}

registerNodeForwardTarget(simpleName: SimpleName, fowardTarget: DocumentTarget) {
registerNodeForwardTarget(simpleName: SimpleName, fowardTarget: DocumentTarget): void {
const existingTarget = this._simpleNameToTarget.get(simpleName)
if (existingTarget && !(existingTarget.target === fowardTarget.target && existingTarget.isAlias === fowardTarget.isAlias)) {
throw new Error(`Duplicate simpleName:"${simpleName}"`)
}

this._simpleNameToTarget.set(simpleName, fowardTarget)
}

registerNodeAsTargetable(node: RstNode): DocumentTarget {
registerNodeAsTargetable(node: RstNode): void {
const simpleName = this.getSimpleName(node)
const target = {
target: `#${simpleName}`,
isAlias: false,
}

this._simpleNameToTarget.set(simpleName, target)
this.registerNodeForwardTarget(simpleName, target)
this._htmlAttrResolver.markNodeAsTargeted(node)

return target
}

// ------------------------------------------------------------------------
// MARK: External Targetable Nodes
// ------------------------------------------------------------------------

registerExternalTargetableNode(simpleName: SimpleName, targetNode?: RstNode) {
if (!targetNode) {
return
}

registerExternalTargetableNode(simpleName: SimpleName, targetNode: RstNode) {
this._nodesTargetableFromOutside.set(simpleName, targetNode)
this._htmlAttrResolver.markNodeAsTargeted(targetNode)
}
Expand Down Expand Up @@ -424,7 +414,7 @@ export class SimpleNameResolver {
const inlineInternalTargets = this._root.findAllChildren(RstNodeType.InlineInternalTarget)
const hyperlinkRefs = this._root.findAllChildren(RstNodeType.HyperlinkRef)

const resolveHyperlinkTarget = (hyperlinkTarget: RstHyperlinkTarget): DocumentTarget | null => {
const resolveHyperlinkTarget = (hyperlinkTarget: RstHyperlinkTarget): DocumentTarget & { targetNode?: RstNode } | null => {
if (hyperlinkTarget.isTargetingNextNode) {
// Follow the chain to find an explicit name
let currNode: RstNode | null = hyperlinkTarget
Expand All @@ -441,7 +431,8 @@ export class SimpleNameResolver {

if (currNode.willRenderVisibleContent) {
return {
...this.registerNodeAsTargetable(currNode),
target: `#${this.getSimpleName(currNode)}`,
isAlias: false,
targetNode: currNode,
}
}
Expand All @@ -464,7 +455,10 @@ export class SimpleNameResolver {

const simpleName = this.getSimpleName(hyperlinkTarget)
this.registerNodeForwardTarget(simpleName, target)
this.registerExternalTargetableNode(simpleName, target.targetNode)

if (target.targetNode) {
this.registerExternalTargetableNode(simpleName, target.targetNode)
}
}
}

Expand All @@ -473,12 +467,11 @@ export class SimpleNameResolver {
const target: DocumentTarget = {
target: `#${this.getSimpleName(inlineTarget)}`,
isAlias: false,
targetNode: inlineTarget,
}

const simpleName = this.getSimpleName(inlineTarget)
this.registerNodeForwardTarget(simpleName, target)
this.registerExternalTargetableNode(simpleName, target.targetNode)
this.registerExternalTargetableNode(simpleName, inlineTarget)
}
}

Expand Down Expand Up @@ -517,8 +510,35 @@ export class SimpleNameResolver {
}
}

const registerTargetableHyperlinkRefs = () => {
for (const hyperlinkRef of hyperlinkRefs) {
// Explicitly named HyperlinkRef that links elsewhere can be referenced by their label
// e.g. `somelabel <url not same as label>`_ can be referenced by somelabel_
if (!hyperlinkRef.isEmbeded) {
continue
}

// When embeded has label===target, it means there's no label e.g. `<url>`_
// Thus this cannot be targeted by other HyperlinkRefs
if (hyperlinkRef.label === hyperlinkRef.target) {
continue
}

const candidateTargetName = normalizeSimpleName(hyperlinkRef.label)
if (this._simpleNameToTarget.has(candidateTargetName)) {
continue
}

this.registerNodeForwardTarget(candidateTargetName, {
target: hyperlinkRef.target,
isAlias: hyperlinkRef.isAlias,
})
}
}

registerHyperlinkTargets()
registerInlineInternalTargets()
registerHyperlinkRefs()
registerTargetableHyperlinkRefs()
}
}
24 changes: 22 additions & 2 deletions src/RstNode/Inline/HyperlinkRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { sanitizeHtml } from '@/utils/sanitizeHtml.js'
import { RstNodeRegistrar } from '@/Parser/RstNodeRegistrar.js'
import { RstNodeType } from '../RstNodeType.js'
import { parseEmbededRef } from '@/utils/parseEmbededRef.js'
import { normalizeSimpleName } from '@/SimpleName.js'
import { RstGeneratorState } from '@/Generator/RstGeneratorState.js'

// ----------------------------------------------------------------------------
// MARK: Node
Expand Down Expand Up @@ -136,12 +138,30 @@ export const hyperlinkRefGenerators = createNodeGenerators(
RstNodeType.HyperlinkRef,

(generatorState, node) => {
const url = generatorState.resolveNodeToUrl(node)
const url = getHyperlinkRefUrl(generatorState, node)
generatorState.writeTextWithLinePrefix(`<a href="${url}">${sanitizeHtml(node.label)}</a>`)
},

(generatorState, node) => {
const url = generatorState.resolveNodeToUrl(node)
const url = getHyperlinkRefUrl(generatorState, node)
generatorState.writeTextWithLinePrefix(`[${sanitizeHtml(node.label)}](${url})`)
},
)

// ----------------------------------------------------------------------------
// MARK: Helpers
// ----------------------------------------------------------------------------

function getHyperlinkRefUrl(generatorState: RstGeneratorState, node: RstHyperlinkRef): string {
// HyperlinkRefs can be written without space between label and angle brackets
// Some expect the angle brackets to contain the target url
// Others expect the whole string as label
// Thus we need to test both cases
if (node.isEmbeded) {
const simpleName = generatorState.simpleNameResolver.getSimpleName(node)
const altSimpleName = normalizeSimpleName(`${node.label}<${node.target}>`)
return generatorState.resolveMultipleSimpleNamesToUrl(node, [altSimpleName, simpleName])
} else {
return generatorState.resolveNodeToUrl(node)
}
}
58 changes: 58 additions & 0 deletions tests/unit/RstNode/ExplicitMarkup/Hyperlink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,4 +829,62 @@ describe('HyperlinkRef', () => {

expect(() => generate()).toThrow(/Failed to resolveNodeToUrl/)
})

describe('when HyperlinkRef has angled brackets as part of its label, it prioritizes HyperlinkTarget with angle brackets', () => {
const goodUrl = 'https://google.ca'
const badUrl = 'https://bing.ca'
const input = `
\`Foo<Bar>\`_
.. _Foo: ${badUrl}
.. _Foo<Bar>: ${goodUrl}
`

testParser(input, [
{
type: RstNodeType.Paragraph,
children: [
{
type: RstNodeType.HyperlinkRef,
text: 'Foo',
data: {
label: 'Foo',
target: 'Bar',
isEmbeded: true,
},
},
],
},
{
type: RstNodeType.HyperlinkTarget,
data: {
label: 'Foo',
target: badUrl,
},
},
{
type: RstNodeType.HyperlinkTarget,
data: {
label: 'Foo<Bar>',
target: goodUrl,
},
},
])

testGenerator(input, `
<p>
<a href="${goodUrl}">Foo</a>
</p>
<!-- HyperlinkTarget id:3 children:0 label:"Foo" target:"${badUrl}" resolvedUrl:"${badUrl}" -->
<!-- HyperlinkTarget id:4 children:0 label:"Foo<Bar>" target:"${goodUrl}" resolvedUrl:"${goodUrl}" -->
`, `
[Foo](${goodUrl})
[HyperlinkTarget id:3 children:0 label:"Foo" target:"${badUrl}" resolvedUrl:"${badUrl}"]: #
[HyperlinkTarget id:4 children:0 label:"Foo<Bar>" target:"${goodUrl}" resolvedUrl:"${goodUrl}"]: #
`)
})
})

0 comments on commit 122d3b8

Please sign in to comment.