Skip to content

Commit

Permalink
enhance: 增强 include 使用 <<<include(xxxx#region)
Browse files Browse the repository at this point in the history
  • Loading branch information
zyao89 committed Oct 26, 2021
1 parent 909eba0 commit bf20fc9
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 71 deletions.
2 changes: 1 addition & 1 deletion theme/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ module.exports = (options, ctx) => {
config.plugin('task-lists').use(require('markdown-it-task-lists'), [{ label: true, labelAfter: true }]);

// include
config.plugin('include').use(require('markdown-it-include'), [ {
config.plugin('include').use(require('./plugins/markdown/markdown-it-include'), [ {
root: sourceDir, // root path
includeRe: /<<<include(.+)/i,
bracesAreOptional: true,
Expand Down
120 changes: 120 additions & 0 deletions theme/plugins/markdown/markdown-it-include.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

const path = require('path');
const fs = require('fs');
const { dedent, findRegion } = require('./snippet/util');

const INCLUDE_RE = /!{3}\s*include(.+?)!{3}/i;
const BRACES_RE = /\((.+?)\)/i;

const include_plugin = (md, options) => {
const defaultOptions = {
root: '.',
getRootDir: (pluginOptions/*, state, startLine, endLine*/) => pluginOptions.root,
includeRe: INCLUDE_RE,
throwError: true,
bracesAreOptional: false,
notFoundMessage: 'File \'{{FILE}}\' not found.',
circularMessage: 'Circular reference between \'{{FILE}}\' and \'{{PARENT}}\'.'
};

if (typeof options === 'string') {
options = {
...defaultOptions,
root: options
};
} else {
options = {
...defaultOptions,
...options
};
}

const _replaceIncludeByContent = (src, rootdir, parentFilePath, filesProcessed) => {
filesProcessed = filesProcessed ? filesProcessed.slice() : []; // making a copy
let cap, filePath, mdSrc, errorMessage, regionName;

// store parent file path to check circular references
if (parentFilePath) {
filesProcessed.push(parentFilePath);
}
while ((cap = options.includeRe.exec(src))) {
let includePath = cap[1].trim();
const sansBracesMatch = BRACES_RE.exec(includePath);

if (!sansBracesMatch && !options.bracesAreOptional) {
errorMessage = `INCLUDE statement '${src.trim()}' MUST have '()' braces around the include path ('${includePath}')`;
} else if (sansBracesMatch) {
includePath = sansBracesMatch[1].trim();
} else if (!/^\s/.test(cap[1])) {
// path SHOULD have been preceeded by at least ONE whitespace character!
/* eslint max-len: "off" */
errorMessage = `INCLUDE statement '${src.trim()}': when not using braces around the path ('${includePath}'), it MUST be preceeded by at least one whitespace character to separate the include keyword and the include path.`;
}

if (!errorMessage) {
const [ _includePath, _regionName ] = includePath ? includePath.split('#') : [''];
filePath = path.resolve(rootdir, _includePath);
regionName = _regionName;

// check if child file exists or if there is a circular reference
if (!fs.existsSync(filePath)) {
// child file does not exist
errorMessage = options.notFoundMessage.replace('{{FILE}}', filePath);
} else if (filesProcessed.indexOf(filePath) !== -1) {
// reference would be circular
errorMessage = options.circularMessage.replace('{{FILE}}', filePath).replace('{{PARENT}}', parentFilePath);
}
}

// check if there were any errors
if (errorMessage) {
if (options.throwError) {
throw new Error(errorMessage);
}
mdSrc = `\n\n# INCLUDE ERROR: ${errorMessage}\n\n`;
} else {
// get content of child file
mdSrc = fs.readFileSync(filePath, 'utf8');
// check if child file also has includes
mdSrc = _replaceIncludeByContent(mdSrc, path.dirname(filePath), filePath, filesProcessed);

if (regionName) {
const lines = mdSrc.split(/\r?\n/)
const region = findRegion(lines, regionName)

if (region) {
mdSrc = dedent(
lines
.slice(region.start, region.end)
.filter(line => !region.regexp.test(line.trim()))
.join('\n')
)
}
}

// remove one trailing newline, if it exists: that way, the included content does NOT
// automatically terminate the paragraph it is in due to the writer of the included
// part having terminated the content with a newline.
// However, when that snippet writer terminated with TWO (or more) newlines, these, minus one,
// will be merged with the newline after the #include statement, resulting in a 2-NL paragraph
// termination.
const len = mdSrc.length;
if (mdSrc[len - 1] === '\n') {
mdSrc = mdSrc.substring(0, len - 1);
}
}

// replace include by file content
src = src.slice(0, cap.index) + mdSrc + src.slice(cap.index + cap[0].length, src.length);
}
return src;
};

const _includeFileParts = (state, startLine, endLine/*, silent*/) => {
state.src = _replaceIncludeByContent(state.src, options.getRootDir(options, state, startLine, endLine));
};

md.core.ruler.before('normalize', 'include', _includeFileParts);
};

module.exports = include_plugin;
Original file line number Diff line number Diff line change
@@ -1,73 +1,5 @@
const { fs, logger, path } = require('@vuepress/shared-utils')

function dedent (text) {
const wRegexp = /^([ \t]*)(.*)\n/gm
let match; let minIndentLength = null

while ((match = wRegexp.exec(text)) !== null) {
const [indentation, content] = match.slice(1)
if (!content) continue

const indentLength = indentation.length
if (indentLength > 0) {
minIndentLength
= minIndentLength !== null
? Math.min(minIndentLength, indentLength)
: indentLength
} else break
}

if (minIndentLength) {
text = text.replace(
new RegExp(`^[ \t]{${minIndentLength}}(.*)`, 'gm'),
'$1'
)
}

return text
}

function testLine (line, regexp, regionName, end = false) {
const [full, tag, name] = regexp.exec(line.trim()) || []

return (
full
&& tag
&& name === regionName
&& tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/)
)
}

function findRegion (lines, regionName) {
const regionRegexps = [
/^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java
/^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss
/^#pragma ((?:end)?region) ([\w*-]+)$/, // C, C++
/^<!-- #?((?:end)?region) ([\w*-]+) -->$/, // HTML, markdown
/^#((?:End )Region) ([\w*-]+)$/, // Visual Basic
/^::#((?:end)region) ([\w*-]+)$/, // Bat
/^# ?((?:end)?region) ([\w*-]+)$/ // C#, PHP, Powershell, Python, perl & misc
]

let regexp = null
let start = -1

for (const [lineId, line] of lines.entries()) {
if (regexp === null) {
for (const reg of regionRegexps) {
if (testLine(line, reg, regionName)) {
start = lineId + 1
regexp = reg
break
}
}
} else if (testLine(line, regexp, regionName, true)) {
return { start, end: lineId, regexp }
}
}

return null
}
const { dedent, findRegion } = require('./util');

module.exports = function snippet (md, options = {}) {
const fence = md.renderer.rules.fence
Expand Down Expand Up @@ -144,7 +76,7 @@ module.exports = function snippet (md, options = {}) {
*/
const rawPathRegexp = /^(.+?(?:\.([a-z]+))?)(?:(#[\w-]+))?(?: ?({\d+(?:[,-]\d+)*}))?$/

const rawPath = state.src.slice(start, end).trim().replace(/^@/, root).trim()
const rawPath = state.src.slice(start, end).trim()
const [filename = '', extension = '', region = '', meta = ''] = (rawPathRegexp.exec(rawPath) || []).slice(1)

state.line = startLine + 1
Expand Down
72 changes: 72 additions & 0 deletions theme/plugins/markdown/snippet/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
module.exports = {
dedent, testLine, findRegion,
};

function dedent(text) {
const wRegexp = /^([ \t]*)(.*)\n/gm
let match; let minIndentLength = null

while ((match = wRegexp.exec(text)) !== null) {
const [indentation, content] = match.slice(1)
if (!content) continue

const indentLength = indentation.length
if (indentLength > 0) {
minIndentLength
= minIndentLength !== null
? Math.min(minIndentLength, indentLength)
: indentLength
} else break
}

if (minIndentLength) {
text = text.replace(
new RegExp(`^[ \t]{${minIndentLength}}(.*)`, 'gm'),
'$1'
)
}

return text
}

function testLine(line, regexp, regionName, end = false) {
const [full, tag, name] = regexp.exec(line.trim()) || []

return (
full
&& tag
&& name === regionName
&& tag.match(end ? /^[Ee]nd ?[rR]egion$/ : /^[rR]egion$/)
)
}

function findRegion(lines, regionName) {
const regionRegexps = [
/^\/\/ ?#?((?:end)?region) ([\w*-]+)$/, // javascript, typescript, java
/^\/\* ?#((?:end)?region) ([\w*-]+) ?\*\/$/, // css, less, scss
/^#pragma ((?:end)?region) ([\w*-]+)$/, // C, C++
/^<!-- #?((?:end)?region) ([\w*-]+) -->$/, // HTML, markdown
/^#((?:End )Region) ([\w*-]+)$/, // Visual Basic
/^::#((?:end)region) ([\w*-]+)$/, // Bat
/^# ?((?:end)?region) ([\w*-]+)$/ // C#, PHP, Powershell, Python, perl & misc
]

let regexp = null
let start = -1

for (const [lineId, line] of lines.entries()) {
if (regexp === null) {
for (const reg of regionRegexps) {
if (testLine(line, reg, regionName)) {
start = lineId + 1
regexp = reg
break
}
}
} else if (testLine(line, regexp, regionName, true)) {
return { start, end: lineId, regexp }
}
}

return null
}

0 comments on commit bf20fc9

Please sign in to comment.