Skip to content

Commit

Permalink
feat: add relative path resolution for links
Browse files Browse the repository at this point in the history
  • Loading branch information
makamekm committed Apr 26, 2024
1 parent a57f2eb commit 133be08
Show file tree
Hide file tree
Showing 14 changed files with 213 additions and 62 deletions.
15 changes: 9 additions & 6 deletions src/transform/headings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ function getHref(token: Token) {
return '#' + (token.attrGet('id') || '');
}

export = function getHeadings(tokens: Token[], needFlatListHeadings?: boolean) {
return needFlatListHeadings ? getFlatListHeadings(tokens) : getFilteredHeadings(tokens);
export = function getHeadings(tokens: Token[], needFlatListHeadings?: boolean, href = '') {
return needFlatListHeadings
? getFlatListHeadings(tokens, href)
: getFilteredHeadings(tokens, href);
};

function getFilteredHeadings(tokens: Token[]) {
function getFilteredHeadings(tokens: Token[], href: string) {
const headings: Heading[] = [];
let parents = [headings];
let prevLevel;
Expand All @@ -33,7 +35,7 @@ function getFilteredHeadings(tokens: Token[]) {
if (isHeading && level >= 2) {
const entry = {
title: getTitle(tokens[i + 1]),
href: getHref(tokens[i]),
href: href + getHref(tokens[i]),
level,
};
let closestParent = parents[parents.length - 1];
Expand Down Expand Up @@ -66,7 +68,8 @@ function getFilteredHeadings(tokens: Token[]) {

return headings;
}
function getFlatListHeadings(tokens: Token[]) {

function getFlatListHeadings(tokens: Token[], href: string) {
const headings: Heading[] = [];

for (let i = 0; i < tokens.length; i++) {
Expand All @@ -79,7 +82,7 @@ function getFlatListHeadings(tokens: Token[]) {

headings.push({
title: getTitle(tokens[i + 1]),
href: getHref(tokens[i]),
href: href + getHref(tokens[i]),
level,
});
}
Expand Down
5 changes: 4 additions & 1 deletion src/transform/md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import attrs from 'markdown-it-attrs';
import extractTitle from './title';
import getHeadings from './headings';
import sanitizeHtml from './sanitize';
import {getPublicPath} from './utils';

function initMarkdownit(options: OptionsType) {
const {allowHTML = false, linkify = false, breaks = true, highlightLangs = {}} = options;
Expand Down Expand Up @@ -85,6 +86,8 @@ function initParser(md: MarkdownIt, options: OptionsType, env: EnvType) {
return (input: string) => {
const {extractTitle: extractTitleOption, needTitle, needFlatListHeadings = false} = options;

const href = getPublicPath(options);

let tokens = md.parse(input, env);

if (extractTitleOption) {
Expand All @@ -104,7 +107,7 @@ function initParser(md: MarkdownIt, options: OptionsType, env: EnvType) {
env.title = extractTitle(tokens).title;
}

env.headings = getHeadings(tokens, needFlatListHeadings);
env.headings = getHeadings(tokens, needFlatListHeadings, href);

return tokens;
};
Expand Down
27 changes: 18 additions & 9 deletions src/transform/plugins/anchors/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {bold} from 'chalk';
import GithubSlugger from 'github-slugger';

import {headingInfo} from '../../utils';
import {getPublicPath, headingInfo} from '../../utils';
import {CUSTOM_ID_EXCEPTION, CUSTOM_ID_REGEXP} from './constants';
import StateCore from 'markdown-it/lib/rules_core/state_core';
import Token from 'markdown-it/lib/token';
Expand All @@ -10,14 +10,20 @@ import {MarkdownItPluginCb} from '../typings';

const slugify: (str: string, opts: {}) => string = require('slugify');

function createLinkTokens(state: StateCore, id: string, title: string, setId = false) {
function createLinkTokens(
state: StateCore,
id: string,
title: string,
setId = false,
href: string,
) {
const open = new state.Token('link_open', 'a', 1);
const close = new state.Token('link_close', 'a', -1);

if (setId) {
open.attrSet('id', id);
}
open.attrSet('href', '#' + id);
open.attrSet('href', href + '#' + id);
open.attrSet('class', 'yfm-anchor');
open.attrSet('aria-hidden', 'true');

Expand Down Expand Up @@ -58,6 +64,7 @@ const removeCustomId = (content: string) => {

return content;
};

const removeCustomIds = (token: Token) => {
token.content = removeCustomId(token.content);
token.children?.forEach((child) => {
Expand All @@ -68,18 +75,20 @@ const removeCustomIds = (token: Token) => {
interface Options {
extractTitle?: boolean;
supportGithubAnchors?: boolean;
transformLink: (v: string) => string;
}

const index: MarkdownItPluginCb<Options> = (
md,
{extractTitle, path, log, supportGithubAnchors},
) => {
const index: MarkdownItPluginCb<Options> = (md, options) => {
const {extractTitle, path, log, supportGithubAnchors} = options;

const plugin = (state: StateCore) => {
/* Do not use the plugin if it is included in the file */
if (state.env.includes && state.env.includes.length) {
return;
}

const href = getPublicPath(options, state.env.path);

const ids: Record<string, number> = {};
const tokens = state.tokens;
let i = 0;
Expand Down Expand Up @@ -132,12 +141,12 @@ const index: MarkdownItPluginCb<Options> = (
const anchorTitle = removeCustomId(title).replace(/`/g, '');
allAnchorIds.forEach((customId) => {
const setId = id !== customId;
const linkTokens = createLinkTokens(state, customId, anchorTitle, setId);
const linkTokens = createLinkTokens(state, customId, anchorTitle, setId, href);

inlineToken.children?.unshift(...linkTokens);

if (supportGithubAnchors) {
const ghLinkTokens = createLinkTokens(state, ghId, anchorTitle, true);
const ghLinkTokens = createLinkTokens(state, ghId, anchorTitle, true, href);
inlineToken.children?.unshift(...ghLinkTokens);
}
});
Expand Down
3 changes: 1 addition & 2 deletions src/transform/plugins/links/collect.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import MarkdownIt from 'markdown-it';
import {sep} from 'path';
import url from 'url';
import {getHrefTokenAttr, isLocalUrl} from '../../utils';
import {PAGE_LINK_REGEXP, getHrefTokenAttr, isLocalUrl} from '../../utils';
import {getSinglePageAnchorId, resolveRelativePath} from '../../utilsFS';
import index from './index';
import {PAGE_LINK_REGEXP} from './constants';

const replaceLinkHref = (input: string, href: string, newHref: string) => {
/* Try not replace include syntax */
Expand Down
1 change: 0 additions & 1 deletion src/transform/plugins/links/constants.ts

This file was deleted.

26 changes: 12 additions & 14 deletions src/transform/plugins/links/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import url from 'url';
import {bold} from 'chalk';
import {findBlockTokens, getHrefTokenAttr, headingInfo, isLocalUrl} from '../../utils';
import {
PAGE_LINK_REGEXP,
defaultTransformLink,
findBlockTokens,
getHrefTokenAttr,
getPublicPath,
headingInfo,
isLocalUrl,
} from '../../utils';
import {getFileTokens, isFileExists} from '../../utilsFS';
import {PAGE_LINK_REGEXP} from './constants';
import Token from 'markdown-it/lib/token';
import {Logger} from 'src/transform/log';
import {MarkdownItPluginCb, MarkdownItPluginOpts} from '../typings';
import path, {isAbsolute, parse, relative, resolve} from 'path';
import path, {isAbsolute, resolve} from 'path';
import {StateCore} from 'src/transform/typings';

function defaultTransformLink(href: string) {
const parsed = url.parse(href);

return url.format({
...parsed,
pathname: parsed.pathname?.replace(PAGE_LINK_REGEXP, '.html'),
});
}

function getTitleFromTokens(tokens: Token[]) {
let title = '';

Expand Down Expand Up @@ -164,8 +162,8 @@ function processLink(state: StateCore, tokens: Token[], idx: number, opts: ProcO
}

let newPathname = '';
if (!isAbsolute(href) && currentPath !== startPath) {
newPathname = relative(parse(startPath).dir, file);
if (!isAbsolute(href)) {
newPathname = getPublicPath(opts, file);

href = url.format({
...url.parse(href),
Expand Down
1 change: 1 addition & 0 deletions src/transform/plugins/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface MarkdownItPluginOpts {
log: Logger;
lang: 'ru' | 'en' | 'es' | 'fr' | 'cs' | 'ar' | 'he';
root: string;
rootPublicPath: string;
isLintRun: boolean;
}

Expand Down
4 changes: 3 additions & 1 deletion src/transform/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ export interface OptionsType {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plugins?: MarkdownItPluginCb<any>[];
highlightLangs?: HighlightLangMap;
root?: string;
extractChangelogs?: boolean;
root?: string;
rootPublicPath?: string;
transformLink?: (href: string) => string;
[x: string]: unknown;
}

Expand Down
35 changes: 35 additions & 0 deletions src/transform/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import url from 'url';
import {relative, resolve} from 'path';
import Token from 'markdown-it/lib/token';

export function isLocalUrl(url: string) {
Expand Down Expand Up @@ -89,3 +91,36 @@ export function getHrefTokenAttr(token: Token) {

return href;
}

export const PAGE_LINK_REGEXP = /\.(md|ya?ml)$/i;

export function defaultTransformLink(href: string) {
const parsed = url.parse(href);
href = url.format({
...parsed,
pathname: parsed.pathname?.replace(PAGE_LINK_REGEXP, '.html'),
});

return href;
}

export function getPublicPath(
{
path,
root,
rootPublicPath,
transformLink,
}: {
path?: string;
root?: string;
rootPublicPath?: string;
transformLink?: (href: string) => string;
},
input?: string | null,
) {
const currentPath = input || path || '';
const filePath = relative(resolve(root || '', rootPublicPath || ''), currentPath);
const transformer = transformLink || defaultTransformLink;
const href = transformer(filePath);
return href;
}
38 changes: 19 additions & 19 deletions test/anchors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import anchors from '../src/transform/plugins/anchors';
import {log} from '../src/transform/log';
import transform from '../src/transform';

const mocksPath = require.resolve('./utils.ts');
const mocksPath = require.resolve('./mocks/link.md');
const transformYfm = (text: string) => {
const {
result: {html},
Expand All @@ -24,24 +24,24 @@ describe('Anchors', () => {

it('should add single anchor with auto naming', () => {
expect(transformYfm('## Test\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test"><a href="#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<h2 id="test"><a href="link.html#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content</p>\n',
);
});

it('should add single anchor', () => {
expect(transformYfm('## Test {#test1}\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<h2 id="test1"><a href="link.html#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content</p>\n',
);
});

it('should add multiple anchors', () => {
expect(transformYfm('## Test {#test1} {#test2} {#test3}\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test1">' +
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
'<a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<a id="test3" href="link.html#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
'<a id="test2" href="link.html#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
'<a href="link.html#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content</p>\n',
);
});
Expand All @@ -53,12 +53,12 @@ describe('Anchors', () => {
'\n' +
'Content before include\n' +
'\n' +
'{% include [test](./mocks/include-anchor.md) %}\n',
'{% include [test](./include-anchor.md) %}\n',
),
).toBe(
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<h2 id="test0"><a href="link.html#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content before include</p>\n' +
'<h1 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
'<h1 id="test1"><a href="link.html#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
'<p>Content</p>\n',
);
});
Expand All @@ -70,23 +70,23 @@ describe('Anchors', () => {
'\n' +
'Content before include\n' +
'\n' +
'{% include [test](./mocks/include-multiple-anchors.md) %}\n',
'{% include [test](./include-multiple-anchors.md) %}\n',
),
).toBe(
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<h2 id="test0"><a href="link.html#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content before include</p>\n' +
'<h1 id="test1">' +
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
'<a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
'<a id="test3" href="link.html#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
'<a id="test2" href="link.html#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
'<a href="link.html#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
'<p>Content</p>\n',
);
});

it('should be transliterated correctly', () => {
expect(transformYfm('## Максимальный размер дисков \n' + '\n' + 'Content\n')).toBe(
'<h2 id="maksimalnyj-razmer-diskov">' +
'<a href="#maksimalnyj-razmer-diskov" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Максимальный размер дисков</span></a>' +
'<a href="link.html#maksimalnyj-razmer-diskov" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Максимальный размер дисков</span></a>' +
'Максимальный размер дисков' +
'</h2>\n' +
'<p>Content</p>\n',
Expand All @@ -96,7 +96,7 @@ describe('Anchors', () => {
it('should be removed fences after transliteration', () => {
expect(transformYfm('## `Test`\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test">' +
'<a href="#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a><code>Test</code>' +
'<a href="link.html#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a><code>Test</code>' +
'</h2>\n' +
'<p>Content</p>\n',
);
Expand All @@ -107,13 +107,13 @@ describe('Anchors', () => {
transformYfm(
'Content before include\n' +
'\n' +
'{% include [file](./mocks/folder-with-#-sharp/file-with-#-sharp.md#anchor) %}\n' +
'{% include [file](./folder-with-#-sharp/file-with-#-sharp.md#anchor) %}\n' +
'\n' +
'After include',
),
).toBe(
'<p>Content before include</p>\n' +
'<h2 id="anchor"><a href="#anchor" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Subtitle</span></a>Subtitle</h2>\n' +
'<h2 id="anchor"><a href="link.html#anchor" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Subtitle</span></a>Subtitle</h2>\n' +
'<p>Subcontent</p>\n' +
'<p>After include</p>\n',
);
Expand All @@ -122,7 +122,7 @@ describe('Anchors', () => {
it('should add anchor with auto naming, using entire heading text', () => {
expect(transformYfm('## _Lorem ~~ipsum **dolor** sit~~ amet_\n\nParagraph\n')).toBe(
'<h2 id="lorem-ipsum-dolor-sit-amet">' +
'<a href="#lorem-ipsum-dolor-sit-amet" class="yfm-anchor" aria-hidden="true">' +
'<a href="link.html#lorem-ipsum-dolor-sit-amet" class="yfm-anchor" aria-hidden="true">' +
'<span class="visually-hidden">Lorem ipsum dolor sit amet</span></a>' +
'<em>Lorem <s>ipsum <strong>dolor</strong> sit</s> amet</em></h2>\n' +
'<p>Paragraph</p>\n',
Expand Down
Loading

0 comments on commit 133be08

Please sign in to comment.