Skip to content

Commit

Permalink
Merge pull request #32 from photogabble/patch/v1.1.0/#31
Browse files Browse the repository at this point in the history
FEATURE: Add escaping for hash links
  • Loading branch information
carbontwelve authored May 3, 2024
2 parents 14af35a + 8146215 commit 41b7e68
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 7 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions src/find-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/interlinker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 18 additions & 2 deletions src/wikilink-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,31 @@ 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('#')) {
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);
Expand Down
27 changes: 26 additions & 1 deletion tests/eleventy.test.js
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -98,3 +97,29 @@ test.serial("Sample page (eleventyExcludeFromCollections set true)", async t =>
`<div><p>Hello World, no links, wiki or otherwise will be parsed by the interlinker due to being excluded from collections.</p></div><div></div>`
);
});

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),
`<div><p>Howdy!</p></div><div><a href="/">Something</a></div>`
);

// Embedded page is aware of its embedding
t.is(
normalize(findResultByUrl(results, '/building-a-self-contained-game-in-c-under-2-kilobytes/').content),
`<div><p>Hello world.</p></div><div><a href="/">Something</a></div>`
);

// Embed shows
t.is(
normalize(findResultByUrl(results, '/').content),
`<div><p>This link should be to <a href="/page/hello/#some-heading">a fragment identifier</a>.</p><p><p>Hello world.</p></p></div><div></div>`
);
});
6 changes: 4 additions & 2 deletions tests/find-page-service.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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 ?
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<div>{{ content }}</div>
<div>{%- for link in backlinks %}<a href="{{ link.url }}">{{ link.title }}</a>{%- endfor %}</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Building a self-contained game in C# under 2 kilobytes
layout: default.liquid
---

Hello world.
12 changes: 12 additions & 0 deletions tests/fixtures/sample-with-hash-in-title/eleventy.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = function (eleventyConfig) {
eleventyConfig.addPlugin(
require('../../../index.js'),
);

return {
dir: {
includes: "_includes",
layouts: "_layouts",
}
}
}
8 changes: 8 additions & 0 deletions tests/fixtures/sample-with-hash-in-title/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Something
layout: default.liquid
---

This link should be to [[Hello World # some-heading | a fragment identifier]].

![[Building a self-contained game in C/# under 2 kilobytes]]
6 changes: 6 additions & 0 deletions tests/fixtures/sample-with-hash-in-title/page/hello.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Hello World
layout: default.liquid
---

Howdy!

0 comments on commit 41b7e68

Please sign in to comment.