diff --git a/bin/args.js b/bin/args.js index ab3a1bd4..38a3105d 100644 --- a/bin/args.js +++ b/bin/args.js @@ -6,6 +6,7 @@ module.exports = require('nomnom') .help('Compile ecmarkup documents to html by passing your input file and output file.') .options({ help: { abbr: 'h', flag: true, help: 'Display this help message' }, + watch: { abbr: 'w', flag: true, help: 'Rebuild when files change' }, biblio: { abbr: 'b', metavar: 'FILE', help: 'Write a biblio file to FILE' }, css: { metavar: 'FILE', help: 'Write Emu CSS dependencies to FILE' }, js: { metavar: 'FILE', help: 'Write Emu JS dependencies to FILE' }, diff --git a/bin/ecmarkup.js b/bin/ecmarkup.js index b211c094..00a5ea1f 100644 --- a/bin/ecmarkup.js +++ b/bin/ecmarkup.js @@ -9,27 +9,69 @@ const Promise = require('bluebird'); const Path = require('path'); const fs = require('fs'); const readFile = Promise.promisify(fs.readFile); +const utils = require('../lib/utils'); +const debounce = require('promise-debounce'); function fetch(path) { return readFile(path, 'utf8'); } -ecmarkup.build(args.infile, fetch, args).then(function (spec) { - if (args.biblio) { - fs.writeFileSync(args.biblio, JSON.stringify(spec.exportBiblio()), 'utf8'); - } +const build = debounce(function build() { + return ecmarkup.build(args.infile, fetch, args).then(function (spec) { + if (args.biblio) { + if (args.verbose) { + utils.logVerbose('Writing biblio file to ' + args.biblio); + } + fs.writeFileSync(args.biblio, JSON.stringify(spec.exportBiblio()), 'utf8'); + } - if (args.outfile) { - fs.writeFileSync(args.outfile, spec.toHTML(), 'utf8'); - } else { - process.stdout.write(spec.toHTML()); - } + if (args.outfile) { + if (args.verbose) { + utils.logVerbose('Writing output to ' + args.outfile); + } + fs.writeFileSync(args.outfile, spec.toHTML(), 'utf8'); + } else { + process.stdout.write(spec.toHTML()); + } - if (args.css) { - fs.writeFileSync(args.css, fs.readFileSync(Path.join(__dirname, '../css/elements.css'))); - } + if (args.css) { + if (args.verbose) { + utils.logVerbose('Writing css file to ' + args.css); + } + fs.writeFileSync(args.css, fs.readFileSync(Path.join(__dirname, '../css/elements.css'))); + } - if (args.js) { - fs.writeFileSync(args.js, fs.readFileSync(Path.join(__dirname, '../js/menu.js'))); - } + if (args.js) { + if (args.verbose) { + utils.logVerbose('Writing css file to ' + args.css); + } + fs.writeFileSync(args.js, fs.readFileSync(Path.join(__dirname, '../js/menu.js'))); + } + + return spec; + }); }); + +if (args.watch) { + let watching = {}; + build().then(function(spec) { + let toWatch = spec.imports.concat(args.infile); + + // remove any files that we're no longer watching + Object.keys(watching).forEach(function(file) { + if (toWatch.indexOf(file) === -1) { + watching[file].close(); + delete watching[file]; + } + }); + + // watch any new files + toWatch.forEach(function(file) { + if (!watching[file]) { + watching[file] = fs.watch(file, build); + } + }); + }); +} else { + build(); +} diff --git a/lib/Import.js b/lib/Import.js index c4dcf870..d6b44ab9 100644 --- a/lib/Import.js +++ b/lib/Import.js @@ -8,6 +8,7 @@ module.exports = class Import extends Builder { build(rootDir) { const href = this.node.getAttribute('href'); const importPath = Path.join(rootDir || this.spec.rootDir, href); + this.spec.imports.push(importPath); return this.spec.fetch(importPath) .then(utils.htmlToDoc) diff --git a/lib/Spec.js b/lib/Spec.js index 3b92e4bf..3b81759a 100644 --- a/lib/Spec.js +++ b/lib/Spec.js @@ -38,6 +38,7 @@ module.exports = class Spec { this.doc = doc; this.fetch = fetch; this.subclauses = []; + this.imports = []; this._numberer = ClauseNumbers.iterator(); this._figureCounts = { table: 0, @@ -77,10 +78,10 @@ module.exports = class Spec { buildAll(selector, Builder, opts) { opts = opts || {}; - this._log('Building ' + selector + '...'); let elems; if (typeof selector === 'string') { + this._log('Building ' + selector + '...'); elems = this.doc.querySelectorAll(selector); } else { elems = selector; @@ -157,7 +158,7 @@ module.exports = class Spec { try { data = yaml.safeLoad(block.textContent); } catch (e) { - console.log('Warning: metadata block failed to parse'); + utils.logWarning('metadata block failed to parse'); return; } finally { block.parentNode.removeChild(block); @@ -194,7 +195,7 @@ module.exports = class Spec { exportBiblio() { if (!this.opts.location) { - console.log('Warning: no spec location specified. Biblio not generated. Try --location or setting the location in the document\'s metadata block.'); + utils.logWarning('no spec location specified. Biblio not generated. Try --location or setting the location in the document\'s metadata block.'); return {}; } @@ -277,9 +278,9 @@ module.exports = class Spec { return null; } - _log() { + _log(str) { if (!this.opts.verbose) return; - console.log.apply(console, arguments); + utils.logVerbose(str); } }; diff --git a/lib/Xref.js b/lib/Xref.js index 8313d1e4..0311cfbe 100644 --- a/lib/Xref.js +++ b/lib/Xref.js @@ -9,18 +9,18 @@ module.exports = class Xref extends Builder { const aoid = xref.getAttribute('aoid'); if (href && aoid) { - console.log('Warning: xref can\'t have both href and aoid.'); + utils.logWarning('xref can\'t have both href and aoid.'); return; } if (!href && !aoid) { - console.log('Warning: xref has no href or aoid.'); + utils.logWarning('xref has no href or aoid.'); return; } if (href) { if (href[0] !== '#') { - console.log('Warning: xref to anything other than a fragment id is not supported (is ' + href + '). Try href="#sec-id" instead.'); + utils.logWarning('xref to anything other than a fragment id is not supported (is ' + href + '). Try href="#sec-id" instead.'); return; } @@ -28,7 +28,7 @@ module.exports = class Xref extends Builder { const entry = this.spec.lookupBiblioEntryById(id); if (!entry) { - console.log('Warning: can\'t find clause, production, note or example with id ' + href); + utils.logWarning('can\'t find clause, production, note or example with id ' + href); return; } @@ -52,7 +52,7 @@ module.exports = class Xref extends Builder { buildFigureLink(this.spec, xref, entry.entry, 'Figure'); break; default: - console.log('Warning: found unknown biblio entry (this is a bug, please file it)'); + utils.logWarning('found unknown biblio entry (this is a bug, please file it)'); } } else if (aoid) { const entry = this.spec.biblio.ops[aoid] || this.spec.externalBiblio.ops[aoid]; @@ -62,7 +62,7 @@ module.exports = class Xref extends Builder { return; } - console.log('Warning: can\'t find abstract op with aoid ' + aoid); + utils.logWarning('can\'t find abstract op with aoid ' + aoid); } } @@ -102,7 +102,7 @@ function buildFigureLink(spec, xref, entry, type) { // first need to find the associated clause const clauseEntry = spec.lookupBiblioEntryById(entry.clauseId); if (clauseEntry.type !== 'clause') { - console.log('Warning: could not find parent clause for ' + type + ' id ' + entry.id); + utils.logWarning('could not find parent clause for ' + type + ' id ' + entry.id); } const parentClause = utils.parent(xref, ['EMU-CLAUSE', 'EMU-INTRO', 'EMU-ANNEX']); diff --git a/lib/utils.js b/lib/utils.js index 76b4ad78..12a24678 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,6 +2,7 @@ const jsdom = require('jsdom'); const Promise = require('bluebird'); +const chalk = require('chalk'); exports.htmlToDoc = function (html) { return new Promise(function (res, rej) { @@ -56,3 +57,13 @@ exports.parent = function parent(node, types) { return parent(node.parentNode, types); }; + +exports.logVerbose = function(str) { + let dateString = (new Date()).toISOString(); + console.log(chalk.gray('[' + dateString + '] ') + str); +}; + +exports.logWarning = function(str) { + let dateString = (new Date()).toISOString(); + console.log(chalk.gray('[' + dateString + '] ') + chalk.red('Warning: ' + str)); +}; diff --git a/package.json b/package.json index 5af6e6cd..6cc8fb99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ecmarkup", - "version": "2.0.3", + "version": "2.1.0", "description": "Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.", "main": "lib/ecmarkup.js", "scripts": { @@ -33,13 +33,15 @@ "homepage": "https://github.com/bterlson/ecmarkup", "dependencies": { "bluebird": "^2.6.4", + "chalk": "^1.1.1", "ecmarkdown": "^3.0.1", "grammarkdown": "^0.1.3", "highlight.js": "^8.6.0", "html-escape": "^1.0.2", "js-yaml": "^3.3.1", "jsdom": "^5.0.1", - "nomnom": "^1.8.1" + "nomnom": "^1.8.1", + "promise-debounce": "^1.0.1" }, "devDependencies": { "diff": "^1.4.0", diff --git a/spec/index.html b/spec/index.html index b2b026d9..9073bb35 100644 --- a/spec/index.html +++ b/spec/index.html @@ -35,6 +35,18 @@
Option | Description |
---|---|
watch | Rebuild when files change |
verbose | Print verbose logging info |
biblio | Emit a biblio file to the specified location |
css | Emit the Ecmarkup CSS file to the specified location |
js | Emit the Ecmarkup JS file to the specified location |
toc | Emit table of contents. Boolean, default true. |
old-toc | Emit the old style of table of contents. Boolean, default false. |
Styles are provided by `elements.css` under the `css` directory. The --css command line option will emit the CSS file to a location of your choosing. You may also link to https://bterlson.github.io/ecmarkup/elements.css which will contain the latest styles for the forseeable future (though I don't promise this won't break you if you're not tracking the latest version).
@@ -42,11 +54,7 @@There are a number of settings that allow customizations or enable generation of boilerplate. Metadata can be included in the document and passed on the command line, for example `--no-toc --title "Document 1"`. Metadata given on the command line takes precedence.
To add metadata to your document, use yaml syntax inside a `<pre class=metadata>` element somewhere in the root of your document.
-The following table lists the currently supported metadata:
-Option | Description |
---|---|
toc | Emit table of contents. Boolean, default true. |
All of the command line options can be provided in the metadata block. See the table above for a list (or consult `--help`).
<pre class=metadata>