diff --git a/.travis.yml b/.travis.yml index b5f060bbd..6aa8f5046 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ node_js: - "9" env: + - PACKAGE=idyll-ast - PACKAGE=idyll-compiler - PACKAGE=idyll-components - PACKAGE=idyll-cli diff --git a/appveyor.yml b/appveyor.yml index 3ec7dd501..40318fee1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -28,6 +28,7 @@ test_script: # Output useful info for debugging. - node --version - npm --version + - cd packages/idyll-ast && npm run test && cd ../.. - cd packages/idyll-cli && npm run test && cd ../.. - cd packages/idyll-compiler && npm run test && cd ../.. - cd packages/idyll-components && npm run test && cd ../.. diff --git a/package.json b/package.json index 2d9e0dd2b..2ff075c27 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "license": "MIT", "scripts": { - "postinstall": "cd packages/idyll-compiler && npm run build && cd ../idyll-components && npm run build && cd ../idyll-document && npm run build && cd ../idyll-layouts && npm run build && cd ../idyll-themes && npm run build" + "postinstall": "cd packages/idyll-compiler && npm run build && cd ../idyll-components && npm run build && cd ../idyll-document && npm run build && cd ../idyll-layouts && npm run build && cd ../idyll-themes && npm run build && cd ../idyll-ast && npm run build" }, "devDependencies": { "lerna": "^2.0.0" diff --git a/packages/idyll-ast/.babelrc b/packages/idyll-ast/.babelrc new file mode 100644 index 000000000..a4c7822e3 --- /dev/null +++ b/packages/idyll-ast/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [ "./.babelrc.js" ] +} diff --git a/packages/idyll-ast/.babelrc.js b/packages/idyll-ast/.babelrc.js new file mode 100644 index 000000000..d94e79ef7 --- /dev/null +++ b/packages/idyll-ast/.babelrc.js @@ -0,0 +1,14 @@ +const { BABEL_ENV, NODE_ENV } = process.env; + +module.exports = { + plugins: ['transform-object-rest-spread'], + presets: [ + [ + 'env', + { + loose: true, + modules: BABEL_ENV === 'cjs' || NODE_ENV === 'test' ? 'commonjs' : false, + }, + ], + ], +}; diff --git a/packages/idyll-ast/.gitignore b/packages/idyll-ast/.gitignore new file mode 100644 index 000000000..f06235c46 --- /dev/null +++ b/packages/idyll-ast/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/packages/idyll-ast/.npmignore b/packages/idyll-ast/.npmignore new file mode 100644 index 000000000..65e3ba2ed --- /dev/null +++ b/packages/idyll-ast/.npmignore @@ -0,0 +1 @@ +test/ diff --git a/packages/idyll-ast/.travis.yml b/packages/idyll-ast/.travis.yml new file mode 100644 index 000000000..b26f2c1f5 --- /dev/null +++ b/packages/idyll-ast/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - "6" + - "7" +install: + - npm install diff --git a/packages/idyll-ast/README.md b/packages/idyll-ast/README.md new file mode 100644 index 000000000..06012b3b2 --- /dev/null +++ b/packages/idyll-ast/README.md @@ -0,0 +1,63 @@ + +# idyll-ast + +Utilities for dealing with Idyll AST. This is most likely useful in the context of compiler plugins. + +## Installation + +``` +npm install --save idyll-ast +``` + +## Usage + + +```js +const ast = require('idyll-ast'); +const compile = require('idyll-compiler'); + +compile(` + # idyll markup goes here +`) +.then((ast) => { + const transformedAST = ast.modifyNodesByName(inputAST, 'h1', (node) => { + node = ast.setProperty(node, 'className', 'super-great-header'); + return node; + }) +}) + +``` + +### Plugin Example + +This plugin just appends a new node at the end of the input: + +```js +const ast = require('idyll-ast'); + +module.exports = (inputAST) => { + return ast.appendNodes(inputAST, []); +}; + + +``` + +## API + +* `appendNode(ast, newNode)` +* `appendNodes(ast, newNodes)` +* `createNode(name, props, children)` +* `getChildren(node)` +* `getNodesByName(ast, 'name')` +* `filterChildren(node, filterFunction)` +* `filterNodes(ast, filterFunction)` +* `modifyChildren(node, modFunction)` +* `modifyNodesByName(ast, 'name', modFunction)` +* `getProperty(node, 'propName')` +* `prependNode(ast, newNode)` +* `prependNodes(ast, newNodes)` +* `removeNodesByName(ast, 'name')` +* `setProperties(node, { prop1: value, prop2: value })` +* `setProperty(node, 'prop', value)` +* `removeProperty(node, 'prop')` + diff --git a/packages/idyll-ast/package.json b/packages/idyll-ast/package.json new file mode 100644 index 000000000..7ee766617 --- /dev/null +++ b/packages/idyll-ast/package.json @@ -0,0 +1,40 @@ +{ + "name": "idyll-ast", + "version": "1.0.0", + "description": "Utilities for manipulating Idyll's AST", + "main": "dist/cjs/index.js", + "module": "dist/es/index.js", + "scripts": { + "prebuild": "rimraf dist", + "build:cjs": "cross-env BABEL_ENV=cjs babel src -d dist/cjs", + "build:es": "cross-env BABEL_ENV=es babel src -d dist/es", + "build": "npm run build:es && npm run build:cjs", + "test": "mocha", + "prepublish": "npm run build", + "prepublishOnly": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/idyll-lang/idyll.git" + }, + "keywords": [ + "idyll", + "ast" + ], + "author": "Matthew Conlen", + "license": "MIT", + "bugs": { + "url": "https://github.com/idyll-lang/idyll/issues" + }, + "homepage": "https://github.com/idyll-lang/idyll#readme", + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-preset-env": "^1.6.0", + "cross-env": "^5.0.5", + "expect.js": "^0.3.1", + "mocha": "^3.2.0", + "rimraf": "^2.6.2" + }, + "dependencies": { + } +} diff --git a/packages/idyll-compiler/src/ast.js b/packages/idyll-ast/src/index.js similarity index 84% rename from packages/idyll-compiler/src/ast.js rename to packages/idyll-ast/src/index.js index edeeb506b..4a5fa93ab 100644 --- a/packages/idyll-compiler/src/ast.js +++ b/packages/idyll-ast/src/index.js @@ -17,10 +17,20 @@ */ +const appendNode = function(ast, node) { + return appendNodes(ast, [node]); +}; + const appendNodes = function(ast, nodes) { return [].concat(ast, nodes); }; +const createNode = function(name, props, children) { + let node = [name, [], children || []]; + node = setProperties(node, props || {}); + return node; +}; + const getChildren = function(node) { return node[2] || []; }; @@ -101,6 +111,10 @@ const getProperty = function(node, key) { }); }; +const prependNode = function(ast, node) { + return prependNodes(ast, [node]); +}; + const prependNodes = function(ast, nodes) { return [].concat(nodes, ast); }; @@ -119,13 +133,16 @@ const removeNodesByName = function(ast, name) { const setProperty = function(node, key, value) { let hasSet = false; - node[1].forEach((element) => { + const isArr = Array.isArray(value); + node[1] = node[1].map((element) => { if (element[0] === key) { hasSet = true; + return [element[0], isArr ? value : ['value', value]]; } + return element; }); if (!hasSet) { - node[1] = node[1].concat([[key, value]]); + node[1] = node[1].concat([[key, isArr ? value : ['value', value]]]); } return node; @@ -149,7 +166,9 @@ const removeProperty = function(node, key) { }; module.exports = { + appendNode, appendNodes, + createNode, getChildren, getNodesByName, filterChildren, @@ -157,6 +176,7 @@ module.exports = { modifyChildren, modifyNodesByName, getProperty, + prependNode, prependNodes, removeNodesByName, setProperties, diff --git a/packages/idyll-ast/test/test.js b/packages/idyll-ast/test/test.js new file mode 100644 index 000000000..1cca7b95f --- /dev/null +++ b/packages/idyll-ast/test/test.js @@ -0,0 +1,10 @@ + +var expect = require('expect.js'); +var ast = require('../src'); + +describe('sanity check', function() { + it('should not blow up', function() { + const input = [['div', [], []]]; + expect(ast.getNodesByName(input, 'div')).to.eql([['div', [], []]]); + }); +}); diff --git a/packages/idyll-cli/src/index.js b/packages/idyll-cli/src/index.js index 17d299d73..4c15893fa 100755 --- a/packages/idyll-cli/src/index.js +++ b/packages/idyll-cli/src/index.js @@ -31,7 +31,7 @@ const idyll = (options = {}, cb) => { '_index.html' ), transform: [], - compilerOptions: { + compiler: { }, }, options @@ -44,7 +44,8 @@ const idyll = (options = {}, cb) => { const inputPackage = fs.existsSync(paths.PACKAGE_FILE) ? require(paths.PACKAGE_FILE) : {}; const inputConfig = Object.assign({ components: {}, - transform: [] + transform: [], + compiler: {} }, inputPackage.idyll || {}); for (let key in inputConfig.components) { inputConfig.components[changeCase.paramCase(key)] = inputConfig.components[key]; @@ -53,6 +54,19 @@ const idyll = (options = {}, cb) => { // Handle options that can be provided via options or via package.json opts.transform = options.transform || inputConfig.transform || opts.transform; + opts.compiler = options.compiler || inputConfig.compiler || opts.compiler; + + // Resolve compiler plugins: + if (opts.compiler.postProcessors) { + opts.compiler.postProcessors = opts.compiler.postProcessors.map((processor) => { + try { + return require(processor, { basedir: paths.INPUT_DIR }); + } catch(e) { + console.log(e); + console.warn('\n\nCould not find post-processor plugin: ', processor); + } + }) + } let bs; diff --git a/packages/idyll-cli/src/pipeline/index.js b/packages/idyll-cli/src/pipeline/index.js index 6c6ad67d9..9e84e7eef 100644 --- a/packages/idyll-cli/src/pipeline/index.js +++ b/packages/idyll-cli/src/pipeline/index.js @@ -22,63 +22,65 @@ const build = (opts, paths, inputConfig) => { opts.inputString = fs.readFileSync(paths.IDYLL_INPUT_FILE, 'utf8'); } - return Promise.try( - // this is all synchronous so we wrap it with Promise.try - // to start a Promise chain and turn any synchronous exceptions into a rejection - () => { - const ast = compile(opts.inputString, opts.compilerOptions); - const template = fs.readFileSync(paths.HTML_TEMPLATE_FILE, 'utf8'); + return compile(opts.inputString, opts.compiler || opts.compilerOptions) + .then((ast) => { + return Promise.try( + () => { + // opts.compilerOptions is kept for backwards compatability + const template = fs.readFileSync(paths.HTML_TEMPLATE_FILE, 'utf8'); - output = { - ast: getASTJSON(ast), - components: getComponentsJS(ast, paths, inputConfig), - css: css(opts), - data: getDataJS(ast, paths.DATA_DIR), - syntaxHighlighting: getHighlightJS(ast, paths), - opts: { - ssr: opts.ssr, - theme: opts.theme, - layout: opts.layout - } - }; - if (!opts.ssr) { - output.html = getBaseHTML(ast, template); - } else { - output.html = getHTML( - paths, - ast, - output.components, - output.data, - template, - { - ssr: opts.ssr, - theme: opts.theme, - layout: opts.layout + output = { + ast: getASTJSON(ast), + components: getComponentsJS(ast, paths, inputConfig), + css: css(opts), + data: getDataJS(ast, paths.DATA_DIR), + syntaxHighlighting: getHighlightJS(ast, paths), + opts: { + ssr: opts.ssr, + theme: opts.theme, + layout: opts.layout + } + }; + if (!opts.ssr) { + output.html = getBaseHTML(ast, template); + } else { + output.html = getHTML( + paths, + ast, + output.components, + output.data, + template, + { + ssr: opts.ssr, + theme: opts.theme, + layout: opts.layout + } + ); } - ); + } + ) + + }) + .then(() => { + return bundleJS(opts, paths, output); // create index.js bundle + }) + .then((js) => { + // minify bundle if necessary and store it + if (opts.minify) { + js = UglifyJS.minify(js, {fromString: true, mangle: { keep_fnames: true}}).code; } - } - ) - .then(() => { - return bundleJS(opts, paths, output); // create index.js bundle - }) - .then((js) => { - // minify bundle if necessary and store it - if (opts.minify) { - js = UglifyJS.minify(js, {fromString: true, mangle: { keep_fnames: true}}).code; - } - output.js = js; - }) - .then(() => { - return Promise.all([ - writeFile(paths.JS_OUTPUT_FILE, output.js), - writeFile(paths.CSS_OUTPUT_FILE, output.css), - writeFile(paths.HTML_OUTPUT_FILE, output.html), - ]); - }) - .then(() => { - return output; // return all results - }); + output.js = js; + }) + .then(() => { + return Promise.all([ + writeFile(paths.JS_OUTPUT_FILE, output.js), + writeFile(paths.CSS_OUTPUT_FILE, output.css), + writeFile(paths.HTML_OUTPUT_FILE, output.html), + ]); + }) + .then(() => { + return output; // return all results + }); } const updateCSS = (opts, paths) => { diff --git a/packages/idyll-cli/test/batteries-included/test.js b/packages/idyll-cli/test/batteries-included/test.js index c98778570..00d633311 100644 --- a/packages/idyll-cli/test/batteries-included/test.js +++ b/packages/idyll-cli/test/batteries-included/test.js @@ -48,7 +48,7 @@ let projectBuildResults; beforeAll(done => { idyll({ inputFile: join(PROJECT_DIR, 'index.idl'), - compilerOptions: { + compiler: { spellcheck: false }, minify: false diff --git a/packages/idyll-cli/test/test-project/test.js b/packages/idyll-cli/test/test-project/test.js index 7cd295840..c41ead7cd 100644 --- a/packages/idyll-cli/test/test-project/test.js +++ b/packages/idyll-cli/test/test-project/test.js @@ -57,7 +57,7 @@ beforeAll(done => { layout: 'centered', theme: join(PROJECT_DIR, 'custom-theme.css'), css: join(PROJECT_DIR, 'styles.css'), - compilerOptions: { + compiler: { spellcheck: false }, minify: false @@ -89,7 +89,7 @@ test('options work as expected', () => { datasets: join(PROJECT_DIR, 'data'), transform: [], port: 3000, - compilerOptions: { + compiler: { spellcheck: false }, inputString: fs.readFileSync(join(PROJECT_DIR, 'index.idl'), 'utf-8') diff --git a/packages/idyll-compiler/src/index.js b/packages/idyll-compiler/src/index.js index ad155989a..ebaafc4c0 100644 --- a/packages/idyll-compiler/src/index.js +++ b/packages/idyll-compiler/src/index.js @@ -6,12 +6,12 @@ const { cleanNewlines } = require('./processors/pre'); const { hoistVariables, flattenChildren, cleanResults, makeFullWidth, wrapText } = require('./processors/post'); const matter = require('gray-matter'); -module.exports = function(input, options) { +module.exports = function(input, options, callback) { input = Processor(input).pipe(cleanNewlines).end(); const { content, data } = matter(input.trim()); - options = Object.assign({}, { spellcheck: false, smartquotes: true }, options || {}); + options = Object.assign({}, { spellcheck: false, smartquotes: true, async: true }, options || {}); const lex = Lexer(); let lexResults = '', output = []; try { @@ -27,7 +27,7 @@ module.exports = function(input, options) { console.error(err.message); } - const ret = Processor(output, options) + let astTransform = Processor(output, options) .pipe(hoistVariables) .pipe(flattenChildren) .pipe(makeFullWidth) @@ -35,5 +35,32 @@ module.exports = function(input, options) { .pipe(cleanResults) .end(); - return ret; + if (options.postProcessors) { + + // Turn them all into promises + const promises = options.postProcessors.map((f) => { + return (ast) => { + return new Promise((resolve, reject) => { + if (f.length === 2) { + f(ast, (err, value) => { + if (err) { + return reject(err); + } + resolve(value); + }) + } else { + resolve(f(ast)); + } + }); + } + }) + + return promises.reduce((promise, f, i) => { + return promise.then((val) => { + return f(val); + }); + }, Promise.resolve(astTransform)); + } else { + return options.async ? new Promise((resolve) => resolve(astTransform)) : astTransform; + } } \ No newline at end of file diff --git a/packages/idyll-compiler/src/processors/post.js b/packages/idyll-compiler/src/processors/post.js index 3367c5e99..7bd00e084 100644 --- a/packages/idyll-compiler/src/processors/post.js +++ b/packages/idyll-compiler/src/processors/post.js @@ -1,7 +1,7 @@ const smartquotes = require('smartquotes'); -const { modifyNodesByName, modifyChildren, getNodesByName, prependNodes, removeNodesByName, removeProperty, setProperty, getProperty } = require('../ast'); +const { modifyNodesByName, modifyChildren, getNodesByName, prependNodes, removeNodesByName, removeProperty, setProperty, getProperty } = require('idyll-ast'); const attrConvert = (list) => { diff --git a/packages/idyll-compiler/test/test.js b/packages/idyll-compiler/test/test.js index e35f202fe..76a55d532 100644 --- a/packages/idyll-compiler/test/test.js +++ b/packages/idyll-compiler/test/test.js @@ -154,29 +154,29 @@ describe('compiler', function() { describe('parser', function() { it('should parse a simple string', function() { var input = 'Just a simple string'; - expect(compile(input)).to.eql([['TextContainer', [], [['p', [], ['Just a simple string']]]]]); + expect(compile(input, { async: false })).to.eql([['TextContainer', [], [['p', [], ['Just a simple string']]]]]); }); it('should handle multiple blocks', function() { var input = 'Just a simple string \n\n with some whitespace'; - expect(compile(input)).to.eql([['TextContainer', [], [['p', [], ['Just a simple string ']], ['p', [], ['with some whitespace']]]]]); + expect(compile(input, { async: false })).to.eql([['TextContainer', [], [['p', [], ['Just a simple string ']], ['p', [], ['with some whitespace']]]]]); }); it('should parse a closed var', function() { var input = '[var /]'; - expect(compile(input)).to.eql([['var', [], []]]); + expect(compile(input, { async: false })).to.eql([['var', [], []]]); }); it('should parse a closed component', function() { var input = '[var name:"v1" value:5 /]\n\nJust a simple string plus a component \n\n [VarDisplay var:v1 /]'; - expect(compile(input)).to.eql([['var', [['name', ['value', 'v1']], ['value', ['value', 5]]], []], ['TextContainer', [], [['p', [], ['Just a simple string plus a component ']], ['VarDisplay', [['var', ['variable', 'v1']]] , []]]]]); + expect(compile(input, { async: false })).to.eql([['var', [['name', ['value', 'v1']], ['value', ['value', 5]]], []], ['TextContainer', [], [['p', [], ['Just a simple string plus a component ']], ['VarDisplay', [['var', ['variable', 'v1']]] , []]]]]); }); it('should parse an open component', function() { var input = '[Slideshow currentSlide:1]test test test[/Slideshow]'; - var output = compile(input); + var output = compile(input, { async: false }); }); it('should parse a nested component', function() { var input = '[Slideshow currentSlide:1]text and stuff \n\n lots of newlines.\n\n[OpenComponent key:"val" ][/OpenComponent][/Slideshow]'; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [['Slideshow', [['currentSlide', ['value', 1]]], [ @@ -188,7 +188,7 @@ describe('compiler', function() { }); it('should handle an inline closed component', function() { var input = 'This is a normal text paragraph that [VarDisplay var:var /] has a component embedded in it.'; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [['p', [], [ 'This is a normal text paragraph that ', @@ -214,7 +214,7 @@ describe('compiler', function() { End text `; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [['h2', [], [ 'This is a header'] @@ -249,7 +249,7 @@ And this is a normal paragraph. This is # not a header. End text `; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [['h2', [], [ 'This is a header'] @@ -273,7 +273,7 @@ End text #### This is a header `; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [['h1', [], [ @@ -295,7 +295,7 @@ End text ### 3. This too. `; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [['h1', [], [ @@ -312,7 +312,7 @@ End text it('should parse an open component with a break at the end', function() { var input = '[Slideshow currentSlide:1]text and stuff \n\n [/Slideshow]'; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [['Slideshow', [['currentSlide', ['value', 1]]], @@ -325,7 +325,7 @@ End text }); it('should parse a paragraph and code fence', function() { var input = 'text text text lots of text\n\n\n```\nvar code = true;\n```\n'; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['p', [], ['text text text lots of text']], @@ -335,7 +335,7 @@ End text }); it('should parse a code fence with backticks inside', function() { var input = 'text text text lots of text\n\n\n````\n```\nvar code = true;\n```\n````\n'; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['p', [], ['text text text lots of text']], @@ -345,7 +345,7 @@ End text }); it('should parse inline code with backticks inside', function() { var input = 'text text text lots of text `` `var code = true;` ``\n'; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['p', [], ['text text text lots of text ', ['code', [], ['`var code = true;`']]]] @@ -354,7 +354,7 @@ End text }); it('should handle backticks in a paragraph', function() { var input = "regular text and stuff, then some `code`"; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['p', [], ['regular text and stuff, then some ', ['code', [], ['code']]]] @@ -365,7 +365,7 @@ End text it('should accept negative numbers', function() { var input = "[component prop:-10 /]"; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['component', [['prop', ['value', -10]]], []] @@ -376,7 +376,7 @@ End text it('should accept positive numbers', function() { var input = "[component prop:10 /]"; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['component', [['prop', ['value', 10]]], []] @@ -387,7 +387,7 @@ End text it('should handle booleans', function() { const input = "[component prop:true /]"; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['component', [['prop', ['value', true]]], []] @@ -397,7 +397,7 @@ End text it('should handle booleans in backticks', function() { const input = "[component prop:`true` /]"; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['component', [['prop', ['expression', 'true']]], []] @@ -407,7 +407,7 @@ End text it('should handle italics and bold', function() { const input = "regular text and stuff, then some *italics* and some **bold**."; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['p', [], ['regular text and stuff, then some ', ['em', [], ['italics']], ' and some ', ['strong', [], ['bold']], '.']] @@ -417,7 +417,7 @@ End text it('should handle unordered list', function() { const input = "* this is the first unordered list item\n* this is the second unordered list item"; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['ul', [], [ @@ -430,7 +430,7 @@ End text it('should handle ordered list', function() { const input = "1. this is the first ordered list item\n2. this is the second ordered list item"; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['ol', [], [ @@ -444,7 +444,7 @@ End text it('should handle inline links', function() { const input = "If you want to define an [inline link](https://idyll-lang.github.io) in the standard markdown style, you can do that."; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ["p",[],["If you want to define an ",["a",[["href",["value","https://idyll-lang.github.io"]]],["inline link"]]," in the standard markdown style, you can do that."]] ]] @@ -453,7 +453,7 @@ End text }); it('should handle inline images', function() { const input = "If you want to define an ![inline image](https://idyll-lang.github.io/logo-text.svg) in the standard markdown style, you can do that."; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ["p",[],["If you want to define an ",["img",[["src",["value","https://idyll-lang.github.io/logo-text.svg"]], ["alt", ["value", "inline image"]]],[]]," in the standard markdown style, you can do that."]]] ]] @@ -462,7 +462,7 @@ End text it('should handle lines that start with bold or italic', function() { const input = "**If** I start a line with bold this should work,\nwhat if I\n\n*start with an italic*?"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ["p",[],[["strong",[],["If"]]," I start a line with bold this should work,\nwhat if I"]],["p",[],[["em",[],["start with an italic"]],"?"]] ]] @@ -471,14 +471,14 @@ End text }) it('should handle component name with a period', function() { const input = "This component name has a period separator [component.val /]."; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ["p",[],["This component name has a period separator ",["component.val",[],[]],"."]] ]]]); }) it('should handle component name with multiple periods', function() { const input = "This component name has a period separator [component.val.v /]."; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ["p",[],["This component name has a period separator ",["component.val.v",[],[]],"."]] ]] @@ -487,7 +487,7 @@ End text it('should handle strong text with a p', function() { const input = "**p a**"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['strong', [], ['p a']] ]] @@ -496,7 +496,7 @@ End text it('should handle strong emphasized text using asterisks', function() { const input = "***test***"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['strong', [], [['em', [], ['test']]]] ]] @@ -505,7 +505,7 @@ End text it('should handle strong emphasized text using underscores', function() { const input = "___test___"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['strong', [], [['em', [], ['test']]]] ]] @@ -514,7 +514,7 @@ End text it('should handle strong emphasized text using mixed asterisks and underscores - 1', function() { const input = "_**test**_"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['em', [], [['strong', [], ['test']]]] ]] @@ -522,7 +522,7 @@ End text }) it('should handle strong emphasized text using mixed asterisks and underscores - 2', function() { const input = "**_test_**"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['strong', [], [['em', [], ['test']]]] ]] @@ -531,7 +531,7 @@ End text it('should merge consecutive word blocks', function() { const input = "[Equation]y = 0[/Equation]"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['equation', [], ['y = 0']] ]] @@ -540,7 +540,7 @@ End text it('should not put smartquotes in code blocks', function() { const input = "`Why 'hello' there`"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['code', [], ["Why 'hello' there"]] ]] @@ -548,7 +548,7 @@ End text }) it('should handle a language in a codeblock ', function() { const input = "```json\n{}\n```"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['CodeHighlight', [['language', ['value', 'json']]], ["{}"]] ]] @@ -558,7 +558,7 @@ End text it('should handle an i tag', function() { const input = "[i]not even em[/i]"; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['i', [], ['not even em']] ]] @@ -573,7 +573,7 @@ End text [Slide/] [/Slideshow]`; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['Slideshow', [], [['Slide', [], []], ['Slide', [], []], ['Slide', [], []]] ] ]] @@ -582,7 +582,7 @@ End text it('should handle items nested in a header', function() { const input = `# My header is **bold**!`; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ["h1",[],["My header is ",["strong",[],["bold"]],"!"]] ]] @@ -601,7 +601,7 @@ End text This is not full width `; - expect(compile(input)).to.eql([ + expect(compile(input, { async: false })).to.eql([ ['TextContainer', [], [ ['p', [], @@ -626,7 +626,7 @@ End text *Wow!* `; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['ul', [], [ @@ -641,7 +641,7 @@ End text it('should preserve space between inline blocks - 1', function() { const input = `*text* __other text__`; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['p', [], [ @@ -656,7 +656,7 @@ End text const input = `[em]text[/em] [b]other text[/b]`; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['p', [], [ @@ -673,7 +673,7 @@ End text const input = `[equation display:true]\sum_{j=0}^n x^{j} + \sum x^{k}[/equation]`; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['equation', [['display', ['value', true]]], [ @@ -691,7 +691,7 @@ End text `; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['equation', [['display', ['value', true]]], [ @@ -703,7 +703,7 @@ End text it('should handle code blocks with parens inside', function() { const input = `[code](n - 1)!/2 possible paths[/code]`; - expect(compile(input)).to.eql( + expect(compile(input, { async: false })).to.eql( [ ['TextContainer', [], [ ['code', [], [ @@ -723,7 +723,7 @@ End text // on // ? // `; - // expect(compile(input)).to.eql( + // expect(compile(input, { async: false })).to.eql( // [ // ['TextContainer', [], [ // ['code', [], [ @@ -738,13 +738,97 @@ End text describe('error handling', function() { it('record line and column number of an error', function() { - input = 'This string contains an un-closed component [BadComponent key:"val" ] '; + const input = 'This string contains an un-closed component [BadComponent key:"val" ] '; try { - var output = compile(input); + const output = compile(input, { async: false }); } catch(err) { expect(err.row).to.be(1); expect(err.column).to.be(70); } }); }); + + describe('plugins', function() { + it('should handle a synchronous post-processing plugin', function(done) { + const input = 'Hello World'; + compile(input, { postProcessors: [(ast) => { + return ast.concat([['TextContainer', [], [ ':)' ]]]) + }]}).then(function(output) { + expect(output).to.eql([ + ['TextContainer', [], [ [ 'p', [], ['Hello World']] ]], + ['TextContainer', [], [ ':)' ]] + ]); + done(); + }); + }); + + it('should handle an asynchronous post-processing plugin', function(done) { + const input = 'Hello World'; + compile(input, { postProcessors: [(ast, callback) => { + callback(null, ast.concat([['TextContainer', [], [ ':)' ]]])); + }]}).then(function(output) { + expect(output).to.eql([ + ['TextContainer', [], [ [ 'p', [], ['Hello World']] ]], + ['TextContainer', [], [ ':)' ]] + ]); + done(); + }); + }); + + it('should handle multiple synchronous post-processing plugins', function(done) { + const input = 'Hello World'; + compile(input, { postProcessors: [(ast) => { + return ast.concat([['TextContainer', [], [ ':)' ]]]); + }, (ast) => { + return ast.concat([['TextContainer', [], [ ':(' ]]]); + }]}).then(function(output) { + expect(output).to.eql([ + ['TextContainer', [], [ [ 'p', [], ['Hello World']] ]], + ['TextContainer', [], [ ':)' ]], + ['TextContainer', [], [ ':(' ]], + ]); + done(); + }); + }); + + it('should handle multiple asynchronous post-processing plugins', function(done) { + const input = 'Hello World'; + compile(input, { postProcessors: [(ast, callback) => { + callback(null, ast.concat([['TextContainer', [], [ ':)' ]]])); + }, (ast, callback) => { + callback(null, ast.concat([['TextContainer', [], [ ':(' ]]])); + }]}) + .then(function(output) { + expect(output).to.eql([ + ['TextContainer', [], [ [ 'p', [], ['Hello World']] ]], + ['TextContainer', [], [ ':)' ]], + ['TextContainer', [], [ ':(' ]], + ]); + done(); + }); + }); + + it('should handle mixed synchronous and asynchronous post-processing plugins', function(done) { + const input = 'Hello World'; + compile(input, { postProcessors: [(ast, callback) => { + callback(null, ast.concat([['TextContainer', [], [ '1' ]]])); + }, (ast) => { + return ast.concat([['TextContainer', [], [ '2' ]]]); + }, (ast, callback) => { + callback(null, ast.concat([['TextContainer', [], [ '3' ]]])); + }, (ast) => { + return ast.concat([['TextContainer', [], [ '4' ]]]); + }]}).then(function(output) { + expect(output).to.eql([ + ['TextContainer', [], [ [ 'p', [], ['Hello World']] ]], + ['TextContainer', [], [ '1' ]], + ['TextContainer', [], [ '2' ]], + ['TextContainer', [], [ '3' ]], + ['TextContainer', [], [ '4' ]], + ]); + done(); + }); + }); + + }) }); diff --git a/packages/idyll-docs/components/editor/index.js b/packages/idyll-docs/components/editor/index.js index 3a1e34459..95d2cad40 100644 --- a/packages/idyll-docs/components/editor/index.js +++ b/packages/idyll-docs/components/editor/index.js @@ -1,37 +1,23 @@ import React from 'react' import IdyllEditArea from './edit-area' import IdyllRenderer from './renderer' -import compile from 'idyll-compiler' import GlobalStyles from '../global-styles' -import { hashCode } from './utils' import styles from './styles' class LiveIdyllEditor extends React.PureComponent { + constructor(props) { super(props) const { markup } = props this.state = { - ...this.stateObjectForMarkup(markup), + error: null, initialMarkup: markup, + currentMarkup: markup } } - stateObjectForMarkup = (idyllMarkup, hash = null) => ({ - idyllHash: hash || hashCode(idyllMarkup), - error: null, - ast: compile(idyllMarkup), - }) - setContent(value) { - // console.log('setting content ', value); - try { - const hash = hashCode(value) - if (hash !== this.state.idyllHash) { - this.setState(this.stateObjectForMarkup(value, hash)) - } - } catch (e) { - this.setState({ error: e.message }) - } + this.setState({ currentMarkup: value }); } componentDidCatch(error, info) { @@ -39,9 +25,6 @@ class LiveIdyllEditor extends React.PureComponent { this.setState({ error: error.message }); } - componentDidMount() { - } - componentWillReceiveProps() { } @@ -55,13 +38,13 @@ class LiveIdyllEditor extends React.PureComponent { render() { const { fullscreen } = this.props; - const { initialMarkup, ast, error, idyllHash } = this.state + const { initialMarkup, currentMarkup, error, idyllHash } = this.state return (
{ fullscreen ? null : } - + { error && this.renderError() }