From de3195d389e9fca62393cbd7884111472cb53efd Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 2 May 2024 22:44:48 +0100 Subject: [PATCH 1/6] chore: added breaking test for #31 --- tests/eleventy.test.js | 23 ++++++++++++++++++- .../_layouts/default.liquid | 2 ++ ...f-contained-game-in-c-under-2-kilobytes.md | 6 +++++ .../eleventy.config.js | 12 ++++++++++ .../sample-with-hash-in-title/index.md | 6 +++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/sample-with-hash-in-title/_layouts/default.liquid create mode 100644 tests/fixtures/sample-with-hash-in-title/building-a-self-contained-game-in-c-under-2-kilobytes.md create mode 100644 tests/fixtures/sample-with-hash-in-title/eleventy.config.js create mode 100644 tests/fixtures/sample-with-hash-in-title/index.md diff --git a/tests/eleventy.test.js b/tests/eleventy.test.js index 0d3bcd4..001b826 100644 --- a/tests/eleventy.test.js +++ b/tests/eleventy.test.js @@ -1,5 +1,4 @@ const Eleventy = require("@11ty/eleventy"); -const path = require('node:path'); const {normalize, consoleMockMessages, findResultByUrl, fixturePath} = require('./helpers'); const test = require("ava"); const sinon = require("sinon"); @@ -98,3 +97,25 @@ test.serial("Sample page (eleventyExcludeFromCollections set true)", async t => `

Hello World, no links, wiki or otherwise will be parsed by the interlinker due to being excluded from collections.

` ); }); + +test("Sample page (file with hash in title)", async t => { + + let elev = new Eleventy(fixturePath('sample-with-hash-in-title'), fixturePath('sample-with-hash-in-title/_site'), { + configPath: fixturePath('sample-with-hash-in-title/eleventy.config.js'), + }); + + let results = await elev.toJSON(); + + + // Embedded page is aware of its embedding + t.is( + normalize(findResultByUrl(results, '/building-a-self-contained-game-in-c-under-2-kilobytes/').content), + `

Hello world.

Something
` + ); + + // Embed shows + t.is( + normalize(findResultByUrl(results, '/').content), + `

Hello world.

` + ); +}); diff --git a/tests/fixtures/sample-with-hash-in-title/_layouts/default.liquid b/tests/fixtures/sample-with-hash-in-title/_layouts/default.liquid new file mode 100644 index 0000000..9065374 --- /dev/null +++ b/tests/fixtures/sample-with-hash-in-title/_layouts/default.liquid @@ -0,0 +1,2 @@ +
{{ content }}
+
{%- for link in backlinks %}{{ link.title }}{%- endfor %}
diff --git a/tests/fixtures/sample-with-hash-in-title/building-a-self-contained-game-in-c-under-2-kilobytes.md b/tests/fixtures/sample-with-hash-in-title/building-a-self-contained-game-in-c-under-2-kilobytes.md new file mode 100644 index 0000000..f91107c --- /dev/null +++ b/tests/fixtures/sample-with-hash-in-title/building-a-self-contained-game-in-c-under-2-kilobytes.md @@ -0,0 +1,6 @@ +--- +title: Building a self-contained game in C# under 2 kilobytes +layout: default.liquid +--- + +Hello world. diff --git a/tests/fixtures/sample-with-hash-in-title/eleventy.config.js b/tests/fixtures/sample-with-hash-in-title/eleventy.config.js new file mode 100644 index 0000000..9b10b5c --- /dev/null +++ b/tests/fixtures/sample-with-hash-in-title/eleventy.config.js @@ -0,0 +1,12 @@ +module.exports = function (eleventyConfig) { + eleventyConfig.addPlugin( + require('../../../index.js'), + ); + + return { + dir: { + includes: "_includes", + layouts: "_layouts", + } + } +} diff --git a/tests/fixtures/sample-with-hash-in-title/index.md b/tests/fixtures/sample-with-hash-in-title/index.md new file mode 100644 index 0000000..841d434 --- /dev/null +++ b/tests/fixtures/sample-with-hash-in-title/index.md @@ -0,0 +1,6 @@ +--- +title: Something +layout: default.liquid +--- + +![[Building a self-contained game in C# under 2 kilobytes]] From 86e98649247d97d4b30dc5dcf4a48be7f8698bf4 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 May 2024 20:36:36 +0100 Subject: [PATCH 2/6] doc: document how wikilink parser works --- src/find-page.js | 2 ++ src/wikilink-parser.js | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/find-page.js b/src/find-page.js index 8b44548..516f1e0 100644 --- a/src/find-page.js +++ b/src/find-page.js @@ -13,6 +13,8 @@ const pageLookup = (allPages = [], slugifyFn) => { return true; } + // TODO: is there a need to slug the page title for comparison? We can match on link.name === page.data.title! + if (page.fileSlug === link.slug || (page.data.title && slugifyFn(page.data.title) === link.slug)) { return true; } diff --git a/src/wikilink-parser.js b/src/wikilink-parser.js index 75d857b..c4acd1b 100644 --- a/src/wikilink-parser.js +++ b/src/wikilink-parser.js @@ -34,9 +34,22 @@ module.exports = class WikilinkParser { return this.linkCache.get(link); } + // Wikilinks starting with a ! are considered Embeds e.g. `![[ ident ]]` const isEmbed = link.startsWith('!'); + + // By default, we display the linked page's title (or alias if used for lookup). This can be overloaded by + // defining the link text prefixed by a | character, e.g. `[[ ident | custom link text ]]` const parts = link.slice((isEmbed ? 3 : 2), -2).split("|").map(part => part.trim()); + + // Strip .md and .markdown extensions from the file ident. + // TODO: I am unsure if this is required might need refactoring in (#13) let name = parts[0].replace(/.(md|markdown)\s?$/i, ""); + + // Anchor link identification. This works similar to Obsidian.md except this doesn't look ahead to + // check if the referenced anchor exists. An anchor link can be referenced by a # character in the + // file ident, e.g. `[[ ident#anchor-id ]]`. + // + // This supports escaping by prefixing the # with a /, e.g `[[ Page about C/# ]]` let anchor = null; if (name.includes('#')) { From d4ef479ffdf44b28d51c6f3aa942948b73e5ae32 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 May 2024 20:36:52 +0100 Subject: [PATCH 3/6] doc: add todo item --- src/interlinker.js | 1 + tests/find-page-service.test.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/interlinker.js b/src/interlinker.js index fc4dc06..c2a44b2 100644 --- a/src/interlinker.js +++ b/src/interlinker.js @@ -114,6 +114,7 @@ module.exports = class Interlinker { }); // If a page has defined aliases, then add those to the link map. These must be unique. + // TODO: 1.1.0 keep track of defined aliases and throw exception if duplicates are found if (data.aliases && Array.isArray(data.aliases)) { for (const alias of data.aliases) { diff --git a/tests/find-page-service.test.js b/tests/find-page-service.test.js index 9a8aac2..54115da 100644 --- a/tests/find-page-service.test.js +++ b/tests/find-page-service.test.js @@ -36,9 +36,9 @@ test('pageLookup (find by href)', t => { test('pageLookup (find by wikilink)', t => { t.is(pageDirectory.findByLink({ title: 'Hello World, Title', - name: 'hello-world', + name: 'Hello World, Title', anchor: null, - link: '[[hello-world]]', + link: '[[Hello World, Title]]', slug: 'hello-world', isEmbed: false, }).fileSlug, 'hello-world'); @@ -54,3 +54,5 @@ test('pageLookup (find by alias)', t => { isEmbed: false, }).fileSlug, 'something-else'); }); + +// TODO: add testing when two pages share the same alias, what _should_ happen ? From ea979aaaf664ac91656a338975393ff740879865 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 May 2024 20:37:27 +0100 Subject: [PATCH 4/6] chore: test for anchor links --- tests/eleventy.test.js | 10 +++++++--- tests/fixtures/sample-with-hash-in-title/index.md | 4 +++- tests/fixtures/sample-with-hash-in-title/page/hello.md | 6 ++++++ 3 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/sample-with-hash-in-title/page/hello.md diff --git a/tests/eleventy.test.js b/tests/eleventy.test.js index 001b826..699c728 100644 --- a/tests/eleventy.test.js +++ b/tests/eleventy.test.js @@ -98,14 +98,18 @@ test.serial("Sample page (eleventyExcludeFromCollections set true)", async t => ); }); -test("Sample page (file with hash in title)", async t => { - +test("Sample page (files with hash in title)", async t => { let elev = new Eleventy(fixturePath('sample-with-hash-in-title'), fixturePath('sample-with-hash-in-title/_site'), { configPath: fixturePath('sample-with-hash-in-title/eleventy.config.js'), }); let results = await elev.toJSON(); + // Linked page is aware of its linking + t.is( + normalize(findResultByUrl(results, '/page/hello/').content), + `

Howdy!

Something
` + ); // Embedded page is aware of its embedding t.is( @@ -116,6 +120,6 @@ test("Sample page (file with hash in title)", async t => { // Embed shows t.is( normalize(findResultByUrl(results, '/').content), - `

Hello world.

` + `

This link should be to a fragment identifier.

Hello world.

` ); }); diff --git a/tests/fixtures/sample-with-hash-in-title/index.md b/tests/fixtures/sample-with-hash-in-title/index.md index 841d434..610dbda 100644 --- a/tests/fixtures/sample-with-hash-in-title/index.md +++ b/tests/fixtures/sample-with-hash-in-title/index.md @@ -3,4 +3,6 @@ title: Something layout: default.liquid --- -![[Building a self-contained game in C# under 2 kilobytes]] +This link should be to [[Hello World # some-heading | a fragment identifier]]. + +![[Building a self-contained game in C/# under 2 kilobytes]] diff --git a/tests/fixtures/sample-with-hash-in-title/page/hello.md b/tests/fixtures/sample-with-hash-in-title/page/hello.md new file mode 100644 index 0000000..c4271c4 --- /dev/null +++ b/tests/fixtures/sample-with-hash-in-title/page/hello.md @@ -0,0 +1,6 @@ +--- +title: Hello World +layout: default.liquid +--- + +Howdy! From 50b0f0bc6377951d4518cfbf7a75c9fcff6c1332 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 May 2024 20:38:14 +0100 Subject: [PATCH 5/6] feat: allow escaping of # in page reference --- src/wikilink-parser.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/wikilink-parser.js b/src/wikilink-parser.js index c4acd1b..3ceed47 100644 --- a/src/wikilink-parser.js +++ b/src/wikilink-parser.js @@ -54,8 +54,11 @@ module.exports = class WikilinkParser { if (name.includes('#')) { const nameParts = parts[0].split('#').map(part => part.trim()); - name = nameParts[0]; - anchor = nameParts[1]; + // Allow for escaping a # when prefixed with a / + if (nameParts[0].at(-1) !== '/') { + name = nameParts[0]; + anchor = nameParts[1]; + } } const slug = this.slugifyFn(name); From 8146215b31ee768bbff70150f63f761f630e0c0b Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 3 May 2024 20:38:51 +0100 Subject: [PATCH 6/6] doc: add info on Linking to fragment identifiers --- README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fff4b96..fcf3edb 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,36 @@ module.exports = (eleventyConfig) => { ### Internal Links / Wikilinks -This plugin will parse both Wikilinks and internal anchor links. The Wikilink format is a **page reference** wrapped in double square brackets, for example: `[[Eleventy.js Interlink Plugin]]` will appear as [Eleventy.js Interlink Plugin](https://photogabble.co.uk/projects/eleventyjs-interlink-plugin/). +This plugin will parse both Wikilinks and internal anchor links to build each pages inbound and outbound internal links. -Using the vertical bar (`|`) you can change the text used to display a link. This can be useful when you want to work a link into a sentence without using the title of the file, for example: `[[Eleventy.js Interlink Plugin|custom display text]]` appears as [custom display text](https://www.photogabble.co.uk/projects/eleventyjs-interlink-plugin/). +The Wikilink format is a **page reference** wrapped in double square brackets, for example: `[[Eleventy.js Interlink Plugin]]` will appear as [Eleventy.js Interlink Plugin](https://photogabble.co.uk/projects/eleventyjs-interlink-plugin/). > **NOTE**: By default this plugin will use the `title` front-matter attribute of your pages or one of the aliases (as detailed below) as the **page reference**. +Using the vertical bar (`|`) you can change the text used to display a link. This can be useful when you want to work a link into a sentence without using the title of the file, for example: `[[Eleventy.js Interlink Plugin|custom display text]]` appears as [custom display text](https://www.photogabble.co.uk/projects/eleventyjs-interlink-plugin/). + +### Linking to fragment identifiers + +If you're using a plugin such as [markdown-it-anchor](https://www.npmjs.com/package/markdown-it-anchor) to add _anchor links_ to your headings, or have otherwise added them yourself. You can link to these in your pages by adding a `#` symbol to your page reference. + +For example, `[[Three laws of motion#Second law]]`. + +In cases where you have the `#` in the title of a page you're linking to you can escape using `/` foe example, `[[Programming in /#C, an introduction]]`. + ### Aliases Aliases provide you a way of referencing a file using different names, use the `aliases` property in your font matter to list one or more aliases that can be used to reference the file from a Wiki Link. For example, you might add _AI_ as an alias of a file titled _Artificial Intelligence_ which would then be linkable via `[[AI]]`. +```yaml +--- +title: Artificial Intelligence +aliases: + - AI +--- +``` + +Aliases should be unique identifiers, this plugin will halt the build with an error if it finds two pages sharing the same alias. + ### Linking to Pagination generated pages A common use of pagination in 11ty is [pagination of an object](https://www.11ty.dev/docs/pagination/#paging-an-object) or data file, by default these generated pages aren't included in the all pages collection and therefore are invisible to this plugin unless you set `addAllPagesToCollections: true`.