-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
enhance: 增强 include 使用
<<<include(xxxx#region)
- Loading branch information
Showing
4 changed files
with
195 additions
and
71 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |