-
Notifications
You must be signed in to change notification settings - Fork 124
preserve Markdown reference links #8717
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mejo-
wants to merge
1
commit into
main
Choose a base branch
from
feat/reference_links
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /** | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| import type MarkdownIt from 'markdown-it' | ||
| import type StateInline from 'markdown-it/lib/rules_inline/state_inline.mjs' | ||
|
|
||
| import linkRule from 'markdown-it/lib/rules_inline/link.mjs' | ||
|
|
||
| type RefType = 'full' | 'collapsed' | 'shortcut' | ||
|
|
||
| /** | ||
| * Decide which reference form (if any) the wrapped link rule matched, | ||
| * by inspecting the source range it consumed. | ||
| * | ||
| * @param state - markdown-it inline parser state | ||
| * @param startPos - position the rule started at | ||
| * @param endPos - position the rule ended at | ||
| */ | ||
| function detectReferenceForm( | ||
| state: StateInline, | ||
| startPos: number, | ||
| endPos: number, | ||
| ): { label: string; type: RefType } | undefined { | ||
| const savedPos = state.pos | ||
| const labelEnd = state.md.helpers.parseLinkLabel(state, startPos) | ||
| state.pos = savedPos | ||
| if (labelEnd < 0) { | ||
| return undefined | ||
| } | ||
|
|
||
| const labelStart = startPos + 1 | ||
| const displayLabel = state.src.slice(labelStart, labelEnd) | ||
| const after = labelEnd + 1 | ||
|
|
||
| if (after >= endPos) { | ||
| return { label: displayLabel, type: 'shortcut' } | ||
| } | ||
|
|
||
| const code = state.src.charCodeAt(after) | ||
|
|
||
| if (code === 0x28 /* ( */) { | ||
| // inline form | ||
| return undefined | ||
| } | ||
|
|
||
| if (code !== 0x5b /* [ */) { | ||
| return { label: displayLabel, type: 'shortcut' } | ||
| } | ||
|
|
||
| if (endPos === after + 2) { | ||
| return { label: displayLabel, type: 'collapsed' } | ||
| } | ||
|
|
||
| return { label: state.src.slice(after + 1, endPos - 1), type: 'full' } | ||
| } | ||
|
|
||
| /** | ||
| * Wrap an existing inline rule so that, when it succeeds via the reference | ||
| * branch, the freshly pushed token is tagged with reference metadata. | ||
| * | ||
| * @param original - upstream inline link rule | ||
| */ | ||
| function wrap(original: (state: StateInline, silent: boolean) => boolean) { | ||
| return (state: StateInline, silent: boolean): boolean => { | ||
| const start = state.pos | ||
| if (!original(state, silent)) { | ||
| return false | ||
| } | ||
| if (silent) { | ||
| return true | ||
| } | ||
|
|
||
| const info = detectReferenceForm(state, start, state.pos) | ||
| if (!info) { | ||
| return true | ||
| } | ||
|
|
||
| const targetType = 'link_open' | ||
| const target = state.tokens.findLast((t) => t.type === targetType) | ||
| if (target) { | ||
| target.attrSet('data-md-reference-label', info.label) | ||
| target.attrSet('data-md-reference-type', info.type) | ||
| } | ||
|
|
||
| return true | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * markdown-it plugin: preserve reference-style link syntax across | ||
| * the parse/serialize round-trip. | ||
| * | ||
| * Adds `data-md-reference-label` / `data-md-reference-type` attributes to | ||
| * `link_open` tokens emitted via the reference branch. | ||
| * | ||
| * @param md - markdown-it instance to extend | ||
| */ | ||
| export default function referenceLinks(md: MarkdownIt): void { | ||
| md.inline.ruler.at('link', wrap(linkRule)) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -73,9 +73,8 @@ describe('Markdown though editor', () => { | |||||
| expect(markdownThroughEditor('[test](foo)')).toBe('[test](foo)') | ||||||
| expect(markdownThroughEditor('[test](foo "bar")')).toBe('[test](foo "bar")') | ||||||
| // Issue #2703 | ||||||
| expect(markdownThroughEditor('[bar\\\\]: /uri\n\n[bar\\\\]')).toBe( | ||||||
| '[bar\\\\](/uri)', | ||||||
| ) | ||||||
| const test2703 = '[bar\\\\]\n\n[bar\\\\]: /uri\n' | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has a different order than the original issue. Does it also pass with the original order: [bar\\]: /uri
[bar\\]
Suggested change
|
||||||
| expect(markdownThroughEditor(test2703)).toBe(test2703) | ||||||
| // Issue #4900 | ||||||
| expect(markdownThroughEditor('[`code`](foo)')).toBe('[`code`](foo)') | ||||||
| expect(markdownThroughEditor('[text with `code` inside](foo)')).toBe( | ||||||
|
|
@@ -86,6 +85,28 @@ describe('Markdown though editor', () => { | |||||
| expect(markdownThroughEditor('text [[wikiLink]] more')).toBe( | ||||||
| 'text [[wikiLink]] more', | ||||||
| ) | ||||||
| // Reference-style links (issue #5820) | ||||||
| const referenceShortcutTest = | ||||||
| 'Test with [Case-Sensitive Reference] in it.\n\n[Case-Sensitive Reference]: https://example.org/\n' | ||||||
| expect(markdownThroughEditor(referenceShortcutTest)).toBe( | ||||||
| referenceShortcutTest, | ||||||
| ) | ||||||
| const referenceCollapsedTest = | ||||||
| 'Test with [label][] in it.\n\n[label]: https://example.org/\n' | ||||||
| expect(markdownThroughEditor(referenceCollapsedTest)).toBe( | ||||||
| referenceCollapsedTest, | ||||||
| ) | ||||||
| const referenceFullTest = | ||||||
| 'Test with [display text][label] in it.\n\n[label]: https://example.org/ "title"\n' | ||||||
| expect(markdownThroughEditor(referenceFullTest)).toBe(referenceFullTest) | ||||||
| // References moved to the end of the document | ||||||
| expect( | ||||||
| markdownThroughEditor( | ||||||
| 'Test with [reference] in it.\n\n[reference]: /url\n\nsome extra paragraph\n', | ||||||
| ), | ||||||
| ).toBe( | ||||||
| 'Test with [reference] in it.\n\nsome extra paragraph\n\n[reference]: /url\n', | ||||||
| ) | ||||||
| }) | ||||||
| test('images', () => { | ||||||
| // Inline images | ||||||
|
|
@@ -123,10 +144,6 @@ describe('Markdown though editor', () => { | |||||
| expect(markdownThroughEditor('```\n```')).toBe('```\n```') | ||||||
| }) | ||||||
| test('markdown untouched', () => { | ||||||
| // Issue #2703 | ||||||
| expect(markdownThroughEditor('[bar\\\\]: /uri\n\n[bar\\\\]')).toBe( | ||||||
| '[bar\\\\](/uri)', | ||||||
| ) | ||||||
| expect(markdownThroughEditor('## Test \\')).toBe('## Test \\') | ||||||
| expect(markdownThroughEditor('- [ [asd](sdf)')).toBe('- [ [asd](sdf)') | ||||||
| }) | ||||||
|
|
||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /** | ||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||
| */ | ||
|
|
||
| import markdownit from '../../markdownit/index.js' | ||
|
|
||
| describe('reference style links (markdown-it)', () => { | ||
| it('renders a reference link of type shortcut (omitted label)', () => { | ||
| expect( | ||
| markdownit.render( | ||
| 'text with [Some Reference] in it\n\n[Some Reference]: https://example.org', | ||
| ), | ||
| ).to.equal( | ||
| '<p>text with <a href="https://example.org" data-md-reference-label="Some Reference" data-md-reference-type="shortcut">Some Reference</a> in it</p>\n', | ||
| ) | ||
| }) | ||
|
|
||
| it('renders a reference link of type collapsed (empty label)', () => { | ||
| expect( | ||
| markdownit.render( | ||
| 'text with [Some Reference][] in it\n\n[Some Reference]: https://example.org', | ||
| ), | ||
| ).to.equal( | ||
| '<p>text with <a href="https://example.org" data-md-reference-label="Some Reference" data-md-reference-type="collapsed">Some Reference</a> in it</p>\n', | ||
| ) | ||
| }) | ||
|
|
||
| it('renders a reference link of type full (separate label)', () => { | ||
| expect( | ||
| markdownit.render( | ||
| 'text with [Some Reference][ref] in it\n\n[ref]: https://example.org', | ||
| ), | ||
| ).to.equal( | ||
| '<p>text with <a href="https://example.org" data-md-reference-label="ref" data-md-reference-type="full">Some Reference</a> in it</p>\n', | ||
| ) | ||
| }) | ||
|
|
||
| it('renders a reference link with paragraphs after reference', () => { | ||
| expect( | ||
| markdownit.render( | ||
| 'text with [reference] in it\n\n[reference]: /url\n\nsome extra content.', | ||
| ), | ||
| ).to.equal( | ||
| '<p>text with <a href="/url" data-md-reference-label="reference" data-md-reference-type="shortcut">reference</a> in it</p>\n<p>some extra content.</p>\n', | ||
| ) | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect the replace is meant to remove dangling newlines so we always have one empty line.
It also removes other dangling whitespace:
Not sure if that is an issue. Two spaces at the end of a line is valid markdown for a hard break. However I think we serialize that to a
<br>tag anyways.Does the meaning of quotes or lists change if they have no content?
For example
*->*- is the latter still a list?