Skip to content

Commit ca82906

Browse files
committed
feat: auto detect invalid inbound links
1 parent f8807b4 commit ca82906

File tree

8 files changed

+71
-23
lines changed

8 files changed

+71
-23
lines changed

docs/guide/markdown.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ meta:
1010

1111
## Header Anchors
1212

13-
Headers automatically get anchor links applied. Rendering of anchors can be configured using the [`markdown.anchor`](./config.md#markdownanchor) option.
13+
Headers automatically get anchor links applied. Rendering of anchors can be configured using the [`markdown.anchor`](../config/#markdownanchor) option.
1414

1515
## Links
1616

1717
- Inbound links ending in `.md` or `.html` are converted to `<router-link>` for SPA navigation.
1818

1919
- [Home](/)
20-
- [Configuring Markdown](./config.md#markdown)
20+
- [Configuring Markdown](../config/#markdown)
2121

2222
- Outbound links automatically gets `target="_blank"`:
2323

@@ -93,7 +93,7 @@ meta:
9393

9494
[[toc]]
9595

96-
Rendering of TOC can be configured using the [`markdown.toc`](./config.md#markdowntoc) option.
96+
Rendering of TOC can be configured using the [`markdown.toc`](../config/#markdowntoc) option.
9797

9898
## Custom Containers
9999

docs/guide/using-vue.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Directives also work:
3737

3838
### Access to Site & Page Data
3939

40-
The compiled component does not have any private data but do have access to the [site metadata](./theming.md#site-and-page-metadata). For example:
40+
The compiled component does not have any private data but do have access to the [site metadata](./custom-themes.md#site-and-page-metadata). For example:
4141

4242
**Input**
4343

lib/dev.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ module.exports = async function dev (sourceDir, cliOptions = {}) {
9090
await serve({
9191
compiler,
9292
host: process.env.DEBUG ? '0.0.0.0' : 'localhost',
93-
dev: { logLevel: 'error' },
93+
dev: { logLevel: 'warn' },
9494
hot: { logLevel: 'error' },
9595
logLevel: 'error',
9696
port,

lib/markdown/hoist.js

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
module.exports = md => {
22
const RE = /^<(script|style)(?=(\s|>|$))/i
3-
let hoistedTags
43

54
md.renderer.rules.html_block = (tokens, idx) => {
65
const content = tokens[idx].content
7-
if (hoistedTags && RE.test(content.trim())) {
6+
const hoistedTags = md.__data.hoistedTags || (md.__data.hoistedTags = [])
7+
if (RE.test(content.trim())) {
88
hoistedTags.push(content)
99
return ''
1010
} else {
1111
return content
1212
}
1313
}
14-
15-
md.renderWithHoisting = (...args) => {
16-
hoistedTags = []
17-
const html = md.render(...args)
18-
return {
19-
html,
20-
hoistedTags
21-
}
22-
}
2314
}

lib/markdown/index.js

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ module.exports = ({ markdown = {}}) => {
3232
includeLevel: [2, 3]
3333
}, markdown.toc))
3434

35+
// override render to allow custom plugins return data
36+
const render = md.render
37+
md.render = (...args) => {
38+
md.__data = {}
39+
const html = render.call(md, ...args)
40+
return {
41+
html,
42+
data: md.__data
43+
}
44+
}
45+
3546
// apply user config
3647
if (markdown.config) {
3748
markdown.config(md)

lib/markdown/link.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@ module.exports = md => {
3131
function toRouterLink (token, link) {
3232
link[0] = 'to'
3333
let to = link[1]
34+
35+
// convert link to filename and export it for existence check
36+
const links = md.__data.links || (md.__data.links = [])
37+
links.push(to)
38+
39+
to = to
3440
.replace(/\.md$/, '.html')
35-
.replace(/\.md(#[\w-]*)/, '.html$1')
41+
.replace(/\.md(#[\w-]*)$/, '.html$1')
3642
// normalize links to README/index
3743
if (/^index|readme\.html/i.test(to)) {
3844
to = '/'

lib/webpack/createBaseConfig.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,10 @@ module.exports = function createBaseConfig ({
9292
.end()
9393
.use('markdown-loader')
9494
.loader(require.resolve('./markdownLoader'))
95-
.options({ markdown })
95+
.options({
96+
sourceDir,
97+
markdown
98+
})
9699

97100
config.module
98101
.rule('images')

lib/webpack/markdownLoader.js

+42-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
1+
const fs = require('fs')
2+
const path = require('path')
3+
const hash = require('hash-sum')
14
const { EventEmitter } = require('events')
25
const { getOptions } = require('loader-utils')
36
const yaml = require('yaml-front-matter')
47
const { inferTitle, extractHeaders } = require('../util')
58

69
const cache = new Map()
10+
const devCache = new Map()
711

812
module.exports = function (src) {
13+
// we implement a manual cache here because this loader is chained before
14+
// vue-loader, and will be applied on the same file multiple times when
15+
// selecting the individual blocks.
16+
const file = this.resourcePath
17+
const key = hash(file + src)
18+
const cached = cache.get(key)
19+
if (cached) {
20+
return cached
21+
}
22+
923
const isProd = process.env.NODE_ENV === 'production'
1024
const isServer = this.target === 'node'
11-
const { markdown } = getOptions(this)
25+
const { markdown, sourceDir } = getOptions(this)
1226

1327
const frontmatter = yaml.loadFront(src)
1428
const content = frontmatter.__content
@@ -20,7 +34,7 @@ module.exports = function (src) {
2034

2135
// diff frontmatter and title, since they are not going to be part of the
2236
// returned component, changes in frontmatter do not trigger proper updates
23-
const cachedData = cache.get(this.resourcePath)
37+
const cachedData = devCache.get(file)
2438
if (cachedData && (
2539
cachedData.inferredTitle !== inferredTitle ||
2640
JSON.stringify(cachedData.frontmatter) !== JSON.stringify(frontmatter) ||
@@ -30,20 +44,43 @@ module.exports = function (src) {
3044
module.exports.frontmatterEmitter.emit('update')
3145
}
3246

33-
cache.set(this.resourcePath, {
47+
devCache.set(file, {
3448
headers,
3549
frontmatter,
3650
inferredTitle
3751
})
3852
}
3953

40-
const { html, hoistedTags } = markdown.renderWithHoisting(content)
41-
return (
54+
// the render method has been augmented to allow plugins to
55+
// register data during render
56+
const { html, data: { hoistedTags, links }} = markdown.render(content)
57+
58+
// check if relative links are valid
59+
links && links.forEach(link => {
60+
const filename = link
61+
.replace(/#[\w-]*$/, '')
62+
.replace(/\.html$/, '.md')
63+
.replace(/\/$/, '/README.md')
64+
.replace(/^\//, sourceDir + '/')
65+
const file = path.resolve(path.dirname(this.resourcePath), filename)
66+
if (!fs.existsSync(file)) {
67+
this.emitWarning(
68+
new Error(
69+
`\nFile for relative link "${link}" does not exist.\n` +
70+
`(Resolved file: ${file})\n`
71+
)
72+
)
73+
}
74+
})
75+
76+
const res = (
4277
`<template>\n` +
4378
`<div class="content">${html}</div>\n` +
4479
`</template>\n` +
4580
hoistedTags.join('\n')
4681
)
82+
cache.set(key, res)
83+
return res
4784
}
4885

4986
function headersChanged (a, b) {

0 commit comments

Comments
 (0)