Skip to content

feat: restrictRelativePath option (close #1646) #1647

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
28 changes: 22 additions & 6 deletions packages/markdown/src/plugins/assetsPlugin/assetsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ export interface AssetsPluginOptions {
* Prefix to add to relative assets links
*/
relativePathPrefix?: string

/**
* Use aliases for non-strict relative paths.
*
* This is a path that does not start
* with `./` or `../` or `/` or `<protocol header>`:
* `<img src="path1/path2.png" />`
*
* - If the option is `true`. `path1` is regarded as an alias.
* - If the option is `false`. It is regarded as a relative path.
* - If the option is `"@-perfix"`.
* If the path starts with `@`, `path1` is regarded as an alias;
* Otherwise, it is regarded as a relative path.
*/
aliasSupport?: boolean | '@-perfix'
}

/**
Expand All @@ -23,6 +38,7 @@ export const assetsPlugin: PluginWithOptions<AssetsPluginOptions> = (
{
absolutePathPrependBase = false,
relativePathPrefix = '@source',
aliasSupport = true,
}: AssetsPluginOptions = {},
) => {
// wrap raw image renderer rule
Expand Down Expand Up @@ -52,19 +68,19 @@ export const assetsPlugin: PluginWithOptions<AssetsPluginOptions> = (
tokens[idx].content = tokens[idx].content
// handle src
.replace(
/(<img\b.*?src=)(['"])(.*?)\2/gs,
(_, prefix: string, quote: string, src: string) =>
/(<(img|source|video|audio)\b.*?src=)(['"])(.*?)\3/gs,
(_, prefix: string, tagName: string, quote: string, src: string) =>
`${prefix}${quote}${resolveLink(src.trim(), {
env,
absolutePathPrependBase,
relativePathPrefix,
strict: true,
aliasSupport,
})}${quote}`,
)
// handle srcset
.replace(
/(<img\b.*?srcset=)(['"])(.*?)\2/gs,
(_, prefix: string, quote: string, srcset: string) =>
/(<(img|source|video|audio)\b.*?srcset=)(['"])(.*?)\3/gs,
(_, prefix: string, tagName: string, quote: string, srcset: string) =>
`${prefix}${quote}${srcset
.split(',')
.map((item) =>
Expand All @@ -75,7 +91,7 @@ export const assetsPlugin: PluginWithOptions<AssetsPluginOptions> = (
env,
absolutePathPrependBase,
relativePathPrefix,
strict: true,
aliasSupport,
})}${descriptor.replace(/[ \n]+/g, ' ').trimEnd()}`,
),
)
Expand Down
17 changes: 10 additions & 7 deletions packages/markdown/src/plugins/assetsPlugin/resolveLink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface ResolveLinkOptions {
env: MarkdownEnv
absolutePathPrependBase?: boolean
relativePathPrefix: string
strict?: boolean
aliasSupport?: boolean | '@-perfix'
}

export const resolveLink = (
Expand All @@ -15,7 +15,7 @@ export const resolveLink = (
env,
absolutePathPrependBase = false,
relativePathPrefix,
strict = false,
aliasSupport = false,
}: ResolveLinkOptions,
): string => {
// do not resolve data uri
Expand All @@ -25,11 +25,14 @@ export const resolveLink = (
let resolvedLink = decode(link)

// check if the link is relative path
const isRelativePath = strict
? // in strict mode, only link that starts with `./` or `../` is considered as relative path
/^\.{1,2}\//.test(link)
: // in non-strict mode, link that does not start with `/` and does not have protocol is considered as relative path
!link.startsWith('/') && !/[A-z]+:\/\//.test(link)
const isRelativePath =
aliasSupport === true
? /^\.{1,2}\//.test(link)
: aliasSupport === false
? !link.startsWith('/') && !/[A-z]+:\/\//.test(link)
: !link.startsWith('/') &&
!link.startsWith('@') &&
!/[A-z]+:\/\//.test(link)

// if the link is relative path, and the `env.filePathRelative` exists
// add `@source` alias to the link
Expand Down
86 changes: 86 additions & 0 deletions packages/markdown/tests/plugins/assetsPlugin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,3 +723,89 @@ describe('html <img> tag', () => {
})
})
})

// Complex situations are tested by the previous `img tag`.
// Here, only the situation where the img tag is replaced with other media tags is supplemented.
//
// And test `aliasSupport` option.
describe('html media tag', () => {
describe('single-line', () => {
const source = [
/* src */
// relative paths
'<audio src="./foo.mp3">',
'<video src="./foo.mp4">',
'<source src="./foo.mp4">',
'<audio src="../foo.mp3">',
'<video src="../foo.mp4">',
'<source src="../foo.mp4">',
// absolute paths
'<audio src="/foo.mp3">',
'<video src="/foo.mp4">',
'<source src="/foo.mp4">',
// aliases
'<audio src="@alias/foo.mp3">',
'<video src="@alias/foo.mp4">',
'<source src="@alias/foo.mp4">',
// no-prefix paths
'<audio src="sub2/foo.mp3">',
'<video src="sub2/foo.mp4">',
'<source src="sub2/foo.mp4">',
]

const TEST_CASES: {
description: string
md: MarkdownIt
env: MarkdownEnv
expected: string[]
}[] = [
{
description: 'should handle assets link with default options',
md: MarkdownIt({ html: true }).use(assetsPlugin, {
aliasSupport: '@-perfix',
}),
env: {
filePathRelative: 'sub/foo.md',
},
expected: [
/* src */
// relative paths
'<audio src="@source/sub/foo.mp3">',
'<video src="@source/sub/foo.mp4">',
'<source src="@source/sub/foo.mp4">',
'<audio src="@source/foo.mp3">',
'<video src="@source/foo.mp4">',
'<source src="@source/foo.mp4">',
// absolute paths
'<audio src="/foo.mp3">',
'<video src="/foo.mp4">',
'<source src="/foo.mp4">',
// aliases
'<audio src="@alias/foo.mp3">',
'<video src="@alias/foo.mp4">',
'<source src="@alias/foo.mp4">',
// no-prefix paths
'<audio src="@source/sub/sub2/foo.mp3">',
'<video src="@source/sub/sub2/foo.mp4">',
'<source src="@source/sub/sub2/foo.mp4">',
],
},
]

TEST_CASES.forEach(({ description, md, env, expected }) => {
it(description, () => {
// Note: Media tags are blocks.

// block
expect(md.render(source.join('\n\n'), env)).toEqual(
expected.map((item) => item).join('\n'),
)

// block with leading white space
expect(
md.render(source.map((item) => ` ${item}`).join('\n\n'), env),
).toEqual(expected.map((item) => ` ${item}`).join('\n'))
})
})
})
})