diff --git a/__tests__/unit/node/markdown/plugins/include.test.ts b/__tests__/unit/node/markdown/plugins/include.test.ts
index 293c3331..053bd58e 100644
--- a/__tests__/unit/node/markdown/plugins/include.test.ts
+++ b/__tests__/unit/node/markdown/plugins/include.test.ts
@@ -445,4 +445,90 @@ describe('node/markdown/plugins/include', () => {
expect(warnings[1]).toContain('nope')
expect(warnings[2]).toContain('b.md')
})
+
+ test('keeps relative urls as is with rebaseRelativeUrls: false', async () => {
+ await write('sub/part.md', '\n\n[link](./target.md)\n')
+
+ const { html } = await render('\n', {
+ include: { rebaseRelativeUrls: false }
+ })
+ expect(html).toContain('src="./img.png"')
+ expect(html).toContain('href="./target.html"')
+ expect(html).not.toContain('@include-')
+ })
+
+ test('rebases relative urls inside included files by default', async () => {
+ await write('sub/part.md', '\n\n[link](./target.md)\n')
+
+ const { html } = await render(
+ '\n\n[after](./after.md)\n'
+ )
+ expect(html).toContain('src="./sub/img.png"')
+ expect(html).toContain('href="./sub/target.html"')
+ // links outside the included content are unaffected
+ expect(html).toContain('href="./after.html"')
+ // the internal markers never reach the output
+ expect(html).not.toContain('@include')
+ })
+
+ test('rebases urls through nested includes', async () => {
+ await write(
+ 'a/one.md',
+ 'one\n\n\n\n\n'
+ )
+ await write('b/two.md', '\n')
+
+ const { html } = await render('\n')
+ expect(html).toContain('src="./b/two.png"')
+ expect(html).toContain('src="./a/one.png"')
+ })
+
+ test('rebases urls after an include ending with an html block', async () => {
+ await write(
+ 'sub/part.md',
+ '\n\n
\ntail\n
\n'
+ )
+
+ const { html } = await render(
+ '\n\n\n\n[after](./after.md)\n'
+ )
+ expect(html).toContain('src="./sub/inside.png"')
+ // the stack must be popped even though the marker follows an html block
+ expect(html).toContain('src="./after.png"')
+ expect(html).toContain('href="./after.html"')
+ expect(html).not.toContain('@include-')
+ })
+
+ test('does not leak rebase markers into fenced includes', async () => {
+ await write('sub/part.md', 'partial line\n')
+
+ const { html } = await render(
+ '```md\n\n```\n'
+ )
+ expect(html).toContain('partial line')
+ expect(html).not.toContain('@include-')
+ })
+
+ test('does not leak rebase markers for inline includes', async () => {
+ await write('sub/part.md', 'partial line\n')
+
+ const { html } = await render(
+ 'before after\n\n[link](./x.md)\n'
+ )
+ expect(html).toContain('partial line')
+ expect(html).not.toContain('@include-')
+ // an inline include leaves the surrounding page urls untouched
+ expect(html).toContain('href="./x.html"')
+ })
+
+ test('does not rebase absolute or external urls', async () => {
+ await write(
+ 'sub/part.md',
+ '[ext](https://example.com/x)\n\n[abs](/abs/target.md)\n'
+ )
+
+ const { html } = await render('\n')
+ expect(html).toContain('href="https://example.com/x"')
+ expect(html).toContain('href="/abs/target.html"')
+ })
})
diff --git a/src/node/markdown/markdown.ts b/src/node/markdown/markdown.ts
index 24c225ca..3791465f 100644
--- a/src/node/markdown/markdown.ts
+++ b/src/node/markdown/markdown.ts
@@ -406,9 +406,6 @@ export async function createMarkdownRenderer(
if (options.snippet !== false) {
snippetPlugin(md, srcDir, normalizePluginOptions(options.snippet), logger)
}
- if (options.include !== false) {
- includePlugin(md, srcDir, normalizePluginOptions(options.include), logger)
- }
const containerOptions = normalizePluginOptions(options.container)
if (options.container !== false) {
containerPlugin(md, containerOptions, { locales: options.locales })
@@ -425,6 +422,11 @@ export async function createMarkdownRenderer(
base,
slugify
)
+ // must come after the image and link plugins so that url rebasing runs
+ // before their href/src handling
+ if (options.include !== false) {
+ includePlugin(md, srcDir, normalizePluginOptions(options.include), logger)
+ }
if (options.tableTabIndex !== false) {
tablePlugin(md)
}
diff --git a/src/node/markdown/plugins/include.ts b/src/node/markdown/plugins/include.ts
index b2f7d144..c2236251 100644
--- a/src/node/markdown/plugins/include.ts
+++ b/src/node/markdown/plugins/include.ts
@@ -13,12 +13,25 @@ export interface Options {
* @default false
*/
silent?: boolean
+ /**
+ * Rewrite relative image and link urls inside included markdown files so
+ * they resolve from the included file's location instead of the page
+ * including it.
+ * @default true
+ */
+ rebaseRelativeUrls?: boolean
}
const includeRE = //g
const rangeRE = /\{(\d*),(\d*)\}$/
const regionRE = /#([^\s{]+)$/
const separatorRE = /[\\/]/
+const fenceRE = /^ {0,3}(`{3,}|~{3,})/
+const rebaseMarkerRE = /^[ \t]*[ \t]*$/gm
+
+// per-render stacks of included-file directories, driven by the rebase
+// markers while rendering
+const rebaseStacks = new WeakMap