Comparing version 0.3.1 to 0.4.0
@@ -7,2 +7,14 @@ const fs = require("fs"); | ||
function validateIsFile(pathValue, pathKey) { | ||
let maybeDir; | ||
try { | ||
maybeDir = fs.statSync(pathValue); | ||
} catch (e) { | ||
throw new Error(`Inaccessible "${pathKey}"`); | ||
} | ||
if (!maybeDir.isFile()) { | ||
throw new Error(`Invalid "${pathKey}"`); | ||
} | ||
} | ||
function validateIsDir(pathValue, pathKey) { | ||
@@ -33,5 +45,34 @@ let maybeDir; | ||
const byPathCmdValidators = { | ||
file: validateIsFile, | ||
files: validateIsDir | ||
}; | ||
exports.byPath = async (pwd, opts) => { | ||
const sourcePath = validatePath(pwd, opts._[0], "path"); | ||
let cmd = null; | ||
for (const [name, validate] of Object.entries(byPathCmdValidators)) { | ||
try { | ||
validate(sourcePath); | ||
cmd = name; | ||
break; | ||
} catch {} | ||
} | ||
if (cmd === null) { | ||
throw new Error(`Invalid "path"`); | ||
} else if (cmd === "files") { | ||
opts.sourcePath = path.relative(pwd, sourcePath); | ||
} else { | ||
opts.sourcePath = "."; | ||
} | ||
return exports[cmd](pwd, opts); | ||
}; | ||
exports.file = async (pwd, opts) => { | ||
const cons = opts._cons || console; | ||
const sourceFile = validatePath(pwd, opts._[0], "file"); | ||
validateIsFile(sourceFile, "sourceFile"); | ||
@@ -48,5 +89,6 @@ opts.outputFormat = opts.format; | ||
const sourceRelativePath = path.relative(pwd, sourceFile); | ||
if (opts.update) { | ||
await evaldown.updateFile(sourceRelativePath); | ||
} else { | ||
if (opts.inplace || opts.update) { | ||
await evaldown.processFile(sourceRelativePath); | ||
} | ||
if (!opts.inplace) { | ||
const { targetOutput } = await evaldown.prepareFile(sourceRelativePath); | ||
@@ -61,7 +103,16 @@ cons.log(targetOutput); | ||
const targetPath = validatePathInOpts(pwd, opts, "targetPath"); | ||
// check that the parent directory exists | ||
validateIsDir(path.join(targetPath, ".."), "targetPath"); | ||
// create the output directory if necessary | ||
await fsExtra.ensureDir(targetPath); | ||
let targetPath; | ||
try { | ||
targetPath = validatePathInOpts(pwd, opts, "targetPath"); | ||
// check that the parent directory exists | ||
validateIsDir(path.join(targetPath, ".."), "targetPath"); | ||
// create the output directory if necessary | ||
await fsExtra.ensureDir(targetPath); | ||
} catch (e) { | ||
if (opts.inplace) { | ||
targetPath = null; | ||
} else { | ||
throw e; | ||
} | ||
} | ||
@@ -68,0 +119,0 @@ opts.outputFormat = opts.format; |
@@ -16,4 +16,3 @@ const fs = require("fs").promises; | ||
magicpenFormat: "html", | ||
generateOutput: async (maker, evalOpts) => | ||
(await maker.withInlinedExamples(evalOpts)).toHtml() | ||
generateOutput: async maker => (await maker.withInlinedExamples()).toHtml() | ||
}, | ||
@@ -23,4 +22,3 @@ inlined: { | ||
magicpenFormat: "html", | ||
generateOutput: async (maker, evalOpts) => | ||
(await maker.withInlinedExamples(evalOpts)).toString() | ||
generateOutput: async maker => (await maker.withInlinedExamples()).toText() | ||
}, | ||
@@ -30,4 +28,3 @@ markdown: { | ||
magicpenFormat: "text", | ||
generateOutput: async (maker, evalOpts) => | ||
(await maker.withUpdatedExamples(evalOpts)).toString() | ||
generateOutput: async maker => (await maker.withUpdatedExamples()).toText() | ||
} | ||
@@ -84,2 +81,5 @@ }; | ||
this.wrapper = wrapper; | ||
// target handling | ||
this.inplace = !!options.inplace; | ||
this.update = !!options.update; | ||
@@ -105,6 +105,9 @@ | ||
async makeOutputForContent(fileContent, format) { | ||
const maker = new Markdown(fileContent, { marker: this.marker }); | ||
const maker = new Markdown(fileContent, { | ||
marker: this.marker, | ||
format: this.formatName | ||
}); | ||
// set basic options for evaluation | ||
const evalOpts = { capture: this.capture, format: this.formatName }; | ||
const evalOpts = { capture: this.capture }; | ||
// set globals to be attached if supplied | ||
@@ -114,7 +117,9 @@ if (this.fileGlobals) { | ||
} | ||
// trigger evaluation for this bag of options | ||
await maker.evaluate(evalOpts); | ||
const targetOutput = await format.generateOutput(maker, evalOpts); | ||
const targetOutput = await format.generateOutput(maker); | ||
let sourceOutput; | ||
if (!this.update) { | ||
if (!(this.inplace || this.update)) { | ||
sourceOutput = null; | ||
@@ -125,3 +130,3 @@ } else if (this.formatName === "markdown") { | ||
const markdownFormat = Evaldown.formats.markdown; | ||
sourceOutput = await markdownFormat.generateOutput(maker, evalOpts); | ||
sourceOutput = await markdownFormat.generateOutput(maker); | ||
} | ||
@@ -149,2 +154,3 @@ | ||
return { | ||
sourceFile, | ||
sourceBaseName, | ||
@@ -158,35 +164,12 @@ sourceDirName, | ||
async processFile(sourceFile) { | ||
const { | ||
sourceBaseName, | ||
sourceDirName, | ||
sourceFilePath, | ||
sourceOutput, | ||
targetOutput | ||
} = await this.prepareFile(sourceFile); | ||
const prepared = await this.prepareFile(sourceFile); | ||
debug('processing source file "%s"', sourceFile); | ||
const targetFile = path.join( | ||
sourceDirName, | ||
`${sourceBaseName}${this.targetExtension}` | ||
); | ||
const targetFilePath = path.join(this.targetPath, targetFile); | ||
try { | ||
await fsExtra.ensureDir(path.dirname(targetFilePath)); | ||
const context = { | ||
sourceFile, | ||
targetFile | ||
}; | ||
await fs.writeFile( | ||
targetFilePath, | ||
this.wrapper(targetOutput, context), | ||
"utf8" | ||
); | ||
} catch (e) { | ||
throw new errors.TargetFileError(e); | ||
if (!this.inplace && this.targetPath) { | ||
await this.writeFile(prepared); | ||
} | ||
if (this.update) { | ||
await fs.writeFile(sourceFilePath, sourceOutput, "utf8"); | ||
if (this.inplace || this.update) { | ||
await this.updateFile(prepared); | ||
} | ||
@@ -222,4 +205,4 @@ } | ||
async updateFile(sourceFile) { | ||
const { sourceFilePath, sourceOutput } = await this.prepareFile(sourceFile); | ||
async updateFile(prepared) { | ||
const { sourceFile, sourceFilePath, sourceOutput } = prepared; | ||
@@ -234,2 +217,34 @@ debug('updating source file "%s"', sourceFile); | ||
} | ||
async writeFile(prepared) { | ||
const { | ||
sourceFile, | ||
sourceBaseName, | ||
sourceDirName, | ||
targetOutput | ||
} = prepared; | ||
debug('writing target for source file "%s"', sourceFile); | ||
const targetFile = path.join( | ||
sourceDirName, | ||
`${sourceBaseName}${this.targetExtension}` | ||
); | ||
const targetFilePath = path.join(this.targetPath, targetFile); | ||
try { | ||
await fsExtra.ensureDir(path.dirname(targetFilePath)); | ||
const context = { | ||
sourceFile, | ||
targetFile | ||
}; | ||
await fs.writeFile( | ||
targetFilePath, | ||
this.wrapper(targetOutput, context), | ||
"utf8" | ||
); | ||
} catch (e) { | ||
throw new errors.TargetFileError(e); | ||
} | ||
} | ||
} | ||
@@ -236,0 +251,0 @@ |
@@ -13,3 +13,2 @@ const symbols = { | ||
return function(first, ...rest) { | ||
if (!first) first = ""; | ||
if (typeof first === "string") { | ||
@@ -16,0 +15,0 @@ this[symbols.line](out, first); |
@@ -39,9 +39,7 @@ var vm = require("vm"); | ||
function getOutputString(expect, format, output) { | ||
if (typeof output === "string") { | ||
return output; | ||
} else if (output) { | ||
function getOutputString(expect, format, flags, output) { | ||
if (typeof output === "undefined" && format !== "markdown" && flags.return) { | ||
return `<div style=""> </div>`; | ||
} else { | ||
return expect.inspect(output, 6, format).toString(); | ||
} else { | ||
return ""; | ||
} | ||
@@ -135,3 +133,5 @@ } | ||
const injectedCode = transpiledCode[0]; | ||
const allBlocks = transpiledCode.split(separatorRegexp); | ||
const injectedCode = transpiledCode[0].trimRight(); | ||
if (injectedCode) { | ||
@@ -142,3 +142,3 @@ vm.runInThisContext(injectedCode); | ||
// unpack the transpiled code block for each snippet | ||
const transpiledBlocks = transpiledCode.split(separatorRegexp).slice(1); | ||
const transpiledBlocks = allBlocks.slice(1); | ||
@@ -195,4 +195,4 @@ for (const [i, transpiledSnippet] of transpiledBlocks.entries()) { | ||
output.kind = "result"; | ||
output.html = getOutputString(expectForOutput, "html", result); | ||
output.text = getOutputString(expectForOutput, "text", result); | ||
output.html = getOutputString(expectForOutput, "html", flags, result); | ||
output.text = getOutputString(expectForOutput, "text", flags, result); | ||
} else if (global.console instanceof InspectedConsole) { | ||
@@ -199,0 +199,0 @@ if (global.console[consoleSymbols.isEmpty]()) return output; |
@@ -97,2 +97,3 @@ const snippetRegexp = require("./snippetRegexp"); | ||
codeIndexEnd: -1, | ||
comment: comment || "", | ||
output: null | ||
@@ -99,0 +100,0 @@ }; |
var Snippets = require("./Snippets"); | ||
var cleanStackTrace = require("./cleanStackTrace"); | ||
var createExpect = require("./createExpect"); | ||
var snippetRegexp = require("./snippetRegexp"); | ||
var marked = require("marked-papandreou"); | ||
@@ -104,3 +103,4 @@ | ||
async evaluateSnippets(options) { | ||
async evaluate(options) { | ||
options = this._prepareOptions(options); | ||
const globals = this._prepareGlobals(options); | ||
@@ -111,3 +111,2 @@ const evalOpts = { markdown: this, globals, ...options }; | ||
await snippets.evaluate(evalOpts); | ||
return snippets; | ||
} | ||
@@ -120,9 +119,11 @@ | ||
toString() { | ||
toText() { | ||
return this.content; | ||
} | ||
async withInlinedExamples(options) { | ||
options = this._prepareOptions(options); | ||
const snippets = await this.evaluateSnippets(options); | ||
async withExamples(onSnippet) { | ||
const snippets = this.snippets; | ||
if (!(snippets && snippets.evaluated)) { | ||
throw new Error("snippets were not evaluated"); | ||
} | ||
@@ -132,6 +133,28 @@ let updatedContent = this.content.slice(0); | ||
for (const [index, snippet] of snippets.entries()) { | ||
const { code, lang } = snippet; | ||
const blockLength = snippet.indexEnd - snippet.index; | ||
const blockRendered = onSnippet(snippet, snippets, index); | ||
if (blockRendered === null) { | ||
continue; | ||
} | ||
updatedContent = spliceString( | ||
updatedContent, | ||
snippet.index + blockDelta, | ||
snippet.indexEnd + blockDelta, | ||
blockRendered | ||
); | ||
blockDelta += blockRendered.length - blockLength; | ||
} | ||
return updatedContent; | ||
} | ||
async withInlinedExamples() { | ||
const onSnippet = (snippet, snippets, index) => { | ||
const { code, lang } = snippet; | ||
let blockRendered; | ||
if (lang === "output") { | ||
@@ -159,3 +182,3 @@ // the presence of the matching previous snippet | ||
if (options.format === "inlined") { | ||
if (this.options.format === "inlined") { | ||
blockRendered = blockRendered.replace(/^<div/, "<pre"); // start of the block | ||
@@ -165,58 +188,40 @@ blockRendered = blockRendered.replace(/\/div>$/, "/pre>"); // end of the block | ||
updatedContent = spliceString( | ||
updatedContent, | ||
snippet.index + blockDelta, | ||
snippet.indexEnd + blockDelta, | ||
blockRendered | ||
); | ||
return blockRendered; | ||
}; | ||
blockDelta += blockRendered.length - blockLength; | ||
} | ||
const updatedContent = await this.withExamples(onSnippet); | ||
return new Markdown(updatedContent, this.options); | ||
} | ||
async withUpdatedExamples(options) { | ||
options = this._prepareOptions(options); | ||
const snippets = await this.evaluateSnippets(options); | ||
async withUpdatedExamples() { | ||
const onSnippet = (snippet, snippets, index) => { | ||
const { code, lang, comment } = snippet; | ||
let index = 0; | ||
const updatedContent = this.content.replace( | ||
snippetRegexp, | ||
(block, htmlComments, lang) => { | ||
var currentIndex = index; | ||
index += 1; | ||
var snippet = snippets.get(currentIndex); | ||
if (snippet.lang === "output") { | ||
// the presence of the matching previous snippet | ||
// is gauranteed when we evaluate the snippets | ||
var example = snippets.get(currentIndex - 1); | ||
if (lang === "output") { | ||
// the presence of the matching previous snippet | ||
// is gauranteed when we evaluate the snippets | ||
var example = snippets.get(index - 1); | ||
var output = ""; | ||
if (example.output) { | ||
output = example.output.text; | ||
if ( | ||
example.output.kind === "error" && | ||
snippet.flags.cleanStackTrace | ||
) { | ||
output = cleanStackTrace(output); | ||
} | ||
var output = ""; | ||
if (example.output) { | ||
output = example.output.text; | ||
if ( | ||
example.output.kind === "error" && | ||
snippet.flags.cleanStackTrace | ||
) { | ||
output = cleanStackTrace(output); | ||
} | ||
} | ||
return `${htmlComments || ""}\`\`\`${lang}\n${output}\n\`\`\``; | ||
} else if (snippet.includesLegacyFlags) { | ||
const langFlagsIndex = lang ? lang.indexOf("#") : -1; | ||
let htmlComments; | ||
if (langFlagsIndex > -1) { | ||
lang = lang.slice(0, langFlagsIndex); | ||
htmlComments = flagsToHtmlComment(this.marker, snippet.flags); | ||
htmlComments += "\n"; | ||
} | ||
return `${htmlComments || ""}\`\`\`${lang}\n${snippet.code}\n\`\`\``; | ||
} else { | ||
return block; | ||
} | ||
return `${comment || ""}\`\`\`${lang}\n${output}\n\`\`\``; | ||
} else if (snippet.includesLegacyFlags) { | ||
let htmlComments = flagsToHtmlComment(this.marker, snippet.flags); | ||
if (htmlComments.length > 0) htmlComments += "\n"; | ||
return `${htmlComments}\`\`\`${lang}\n${code}\n\`\`\``; | ||
} else { | ||
return null; | ||
} | ||
); | ||
}; | ||
const updatedContent = await this.withExamples(onSnippet); | ||
return new Markdown(updatedContent, this.options); | ||
@@ -223,0 +228,0 @@ } |
@@ -6,2 +6,3 @@ var extractSnippets = require("./extractSnippets"); | ||
constructor(snippets) { | ||
this.evaluated = false; | ||
this.items = snippets; | ||
@@ -31,4 +32,7 @@ } | ||
async evaluate(options) { | ||
if (this.evaluated) { | ||
throw new Error("the same snippets were evaluated twice"); | ||
} | ||
await evaluateSnippets(this.snippets, options); | ||
return this; | ||
this.evaluated = true; | ||
} | ||
@@ -35,0 +39,0 @@ |
{ | ||
"name": "evaldown", | ||
"version": "0.3.1", | ||
"version": "0.4.0", | ||
"description": "Evalute JavaScript snippets in markdown files and output static pages.", | ||
@@ -5,0 +5,0 @@ "main": "lib/Evaldown.js", |
# Evaldown | ||
Evalute JavaScript snippets in markdown files and output static pages. | ||
Evaluate JavaScript snippets in markdown files. | ||
@@ -9,21 +9,22 @@ [![NPM version](https://img.shields.io/npm/v/evaldown.svg)](https://www.npmjs.com/package/evaldown) | ||
This project will recursively traverse a folder structure searching | ||
for markdown files. Once found, it will extract javascript code blocks, | ||
evaluate them and serialise their pretty-printed output for rendering. | ||
This tool provides both CLI and programmatic interfaces for | ||
locating JavaScript code blocks in one or more markdown files, | ||
extracting and evaluating these blocks and provides a range | ||
formats in which to serialise their pretty-printed output. | ||
The tool can even automatically capture the output of your examples. | ||
See the [updating](#Updating-examples) section for more details. | ||
## Use | ||
Once the tool is installed and configured it can be used via the CLI. | ||
which supports processing a single markdown file or more typically | ||
a directory of files with configuration read from a file. | ||
We start by introducing an invocation for processing a single | ||
markdown file: | ||
### Process single files | ||
``` | ||
./node_modules/.bin/evaldown ./docs/README.md | ||
``` | ||
In single file use, the tool can be invoked as follows: | ||
The file will be processed and the output written to stdout. | ||
In order to store the output within the source file, thereby | ||
automatically capturing it, we can use the `--inplace` option: | ||
``` | ||
./node_modules/.bin/evaldown ./docs/README.md > README.md | ||
./node_modules/.bin/evaldown --inplace ./docs/README.md | ||
``` | ||
@@ -33,12 +34,20 @@ | ||
This mode requires a configuration file. Once written, the tool | ||
is invoked will read the source path, scan it for markdown files | ||
and write output to a target path. This mode is invoked by: | ||
Applying a similiar update to all files within a directory | ||
structure looks almost identical: | ||
``` | ||
./node_modules/.bin/evaldown --inplace ./testdata/ | ||
``` | ||
### Beyond command line options | ||
The tool supports many additional options to alter its behaviour, | ||
and these can all be read directly from a configuration file: | ||
``` | ||
./node_modules/.bin/evaldown --config <path_to_config> | ||
``` | ||
The sections below discuss configuring the the tool and | ||
authoring you first example files. | ||
The sections below discuss configuring the tool and | ||
authoring of examples. | ||
@@ -117,2 +126,14 @@ ## Configuration | ||
### Keeping the source up-to-date | ||
As you change your examples, updating means you can always keep the | ||
output up-to-date. This mode is considered a key use-case and can | ||
enabled by default via the configuration file: | ||
It can also be activaited on the command line on demand: | ||
``` | ||
./node_modules/.bin/evaldown --config <path_to_config> --update | ||
``` | ||
## Authoring | ||
@@ -182,26 +203,1 @@ | ||
</pre> | ||
## Updating examples | ||
Rather than be forced to write the output by hand, we can automatially | ||
execute the provided code snippets and inject their results into the | ||
source markdown files. This is done using the `"update"` option. | ||
As you change your examples, updating means you can always keep the | ||
output up-to-date. This mode is considered a _primary use-case_ and | ||
can be activated by supplying an additional command line option: | ||
``` | ||
./node_modules/.bin/evaldown --config <path_to_config> --update | ||
``` | ||
It can also be placed within the configuration file: | ||
```js | ||
module.exports = { | ||
update: true, | ||
sourcePath: "./input", | ||
targetPath: "./output" | ||
}; | ||
``` |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
44445
1066
200