Skip to content
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

feat: add relative path resolution for links #407

Merged
merged 1 commit into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading