UNPKG

idyll-compiler

Version:
1,582 lines (1,468 loc) 53 kB
import expect from 'expect.js'; import Lexer from '../src/lexer'; import compile from '../src'; import { convertV1ToV2 } from 'idyll-ast'; function appendTextNode(ast, value) { ast.children.push({ type: 'component', name: 'TextContainer', children: [{ type: 'textnode', value }] }); return ast; } describe('compiler', function() { describe('lexer', function() { it('should tokenize the input', function() { var lex = Lexer(); var results = lex('Hello \n\nWorld! []'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "Hello " TOKEN_VALUE_END BREAK WORDS TOKEN_VALUE_START "World" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "! " TOKEN_VALUE_END OPEN_BRACKET CLOSE_BRACKET EOF' ); }); it('should tokenize the input with a complex component', function() { var lex = Lexer(); var results = lex('Hello \n\nWorld \n\n [VarDisplay var:v work:"no" /]'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "Hello " TOKEN_VALUE_END BREAK WORDS TOKEN_VALUE_START "World " TOKEN_VALUE_END BREAK OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "VarDisplay" TOKEN_VALUE_END COMPONENT_WORD TOKEN_VALUE_START "var" TOKEN_VALUE_END PARAM_SEPARATOR COMPONENT_WORD TOKEN_VALUE_START "v" TOKEN_VALUE_END COMPONENT_WORD TOKEN_VALUE_START "work" TOKEN_VALUE_END PARAM_SEPARATOR STRING TOKEN_VALUE_START ""no"" TOKEN_VALUE_END FORWARD_SLASH CLOSE_BRACKET EOF' ); }); it('should support single quotes around strings', function() { var lex = Lexer(); var results = lex("Hello \n\nWorld \n\n [VarDisplay var:v work:'no' /]"); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "Hello " TOKEN_VALUE_END BREAK WORDS TOKEN_VALUE_START "World " TOKEN_VALUE_END BREAK OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "VarDisplay" TOKEN_VALUE_END COMPONENT_WORD TOKEN_VALUE_START "var" TOKEN_VALUE_END PARAM_SEPARATOR COMPONENT_WORD TOKEN_VALUE_START "v" TOKEN_VALUE_END COMPONENT_WORD TOKEN_VALUE_START "work" TOKEN_VALUE_END PARAM_SEPARATOR STRING TOKEN_VALUE_START ""no"" TOKEN_VALUE_END FORWARD_SLASH CLOSE_BRACKET EOF' ); }); it('should recognize headings', function() { var lex = Lexer(); var results = lex('\n## my title'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'BREAK HEADER_2 WORDS TOKEN_VALUE_START "my title" TOKEN_VALUE_END HEADER_END EOF' ); }); it('should recognize block quotes', function() { var lex = Lexer(); var results = lex('\n> My blockquote'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'BREAK QUOTE_START WORDS TOKEN_VALUE_START "My blockquote" TOKEN_VALUE_END QUOTE_END EOF' ); }); it('should handle newlines', function() { var lex = Lexer(); var results = lex('\n\n\n\n text \n\n'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "text" TOKEN_VALUE_END EOF' ); }); it('should ignore escaped brackets', function() { var lex = Lexer(); var results = lex('Text with a range, \\[0, 20\\].'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "Text with a range, " TOKEN_VALUE_END WORDS TOKEN_VALUE_START "[" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "0" TOKEN_VALUE_END WORDS TOKEN_VALUE_START ", " TOKEN_VALUE_END WORDS TOKEN_VALUE_START "2" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "0" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "]" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "." TOKEN_VALUE_END EOF' ); }); it('should handle markdown formating in a list element', function() { var lex = Lexer(); var results = lex('- **test**'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'BREAK UNORDERED_LIST LIST_ITEM STRONG WORDS TOKEN_VALUE_START "test" TOKEN_VALUE_END STRONG_END LIST_END EOF' ); }); it('should handle equations', function() { var lex = Lexer(); var results = lex('[Equation]y = 0[/Equation]'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "equation" TOKEN_VALUE_END CLOSE_BRACKET WORDS TOKEN_VALUE_START "y = 0" TOKEN_VALUE_END OPEN_BRACKET FORWARD_SLASH COMPONENT_NAME TOKEN_VALUE_START "equation" TOKEN_VALUE_END CLOSE_BRACKET EOF' ); }); it('should handle backticks in a paragraph', function() { var lex = Lexer(); var results = lex('regular text and stuff, then some `code`'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "regular text and stuff, then some " TOKEN_VALUE_END INLINE_CODE TOKEN_VALUE_START "code" TOKEN_VALUE_END EOF' ); }); it('should handle a markdown-style link in a paragraph', function() { var lex = Lexer(); var results = lex( 'regular text and stuff, then a [link](https://idyll-lang.github.io/).' ); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "regular text and stuff, then a " TOKEN_VALUE_END LINK TOKEN_VALUE_START "link" TOKEN_VALUE_END TOKEN_VALUE_START "https://idyll-lang.github.io/" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "." TOKEN_VALUE_END EOF' ); }); it('should handle a markdown-style image', function() { var lex = Lexer(); var results = lex( 'This is an image inline ![image](https://idyll-lang.github.io/logo-text.svg).' ); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "This is an image inline " TOKEN_VALUE_END IMAGE TOKEN_VALUE_START "image" TOKEN_VALUE_END TOKEN_VALUE_START "https://idyll-lang.github.io/logo-text.svg" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "." TOKEN_VALUE_END EOF' ); }); it('should handle a component name with a period', function() { var lex = Lexer(); var results = lex( 'This component name has a period separator [component.val /].' ); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "This component name has a period separator " TOKEN_VALUE_END OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "component.val" TOKEN_VALUE_END FORWARD_SLASH CLOSE_BRACKET WORDS TOKEN_VALUE_START "." TOKEN_VALUE_END EOF' ); }); it('should handle a component name with multiple periods', function() { var lex = Lexer(); var results = lex( 'This component name has a period separator [component.val.v /].' ); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "This component name has a period separator " TOKEN_VALUE_END OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "component.val.v" TOKEN_VALUE_END FORWARD_SLASH CLOSE_BRACKET WORDS TOKEN_VALUE_START "." TOKEN_VALUE_END EOF' ); }); it('should handle an i tag', function() { var lex = Lexer(); var results = lex('[i]not even em[/i]'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "i" TOKEN_VALUE_END CLOSE_BRACKET WORDS TOKEN_VALUE_START "not even em" TOKEN_VALUE_END OPEN_BRACKET FORWARD_SLASH COMPONENT_NAME TOKEN_VALUE_START "i" TOKEN_VALUE_END CLOSE_BRACKET EOF' ); }); it('should ignore comments', function() { var lex = Lexer(); var results = lex(` Text. / Not a comment. // Comment // Second comment [component]//not a comment // comment inside components [/component]// is a comment not a comment: https://stuff.com `); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "Text. " TOKEN_VALUE_END WORDS TOKEN_VALUE_START "/ Not a comment.\n " TOKEN_VALUE_END BREAK OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "component" TOKEN_VALUE_END CLOSE_BRACKET WORDS TOKEN_VALUE_START "/" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "/not a comment\n " TOKEN_VALUE_END OPEN_BRACKET FORWARD_SLASH COMPONENT_NAME TOKEN_VALUE_START "component" TOKEN_VALUE_END CLOSE_BRACKET BREAK WORDS TOKEN_VALUE_START "not a comment: https:" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "/" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "/stuff.com" TOKEN_VALUE_END EOF' ); }); it('ordered lists with a space after the bullet are parsed as a list', function() { var input = ` 1. bar `; var lex = Lexer(); var results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'BREAK ORDERED_LIST LIST_ITEM WORDS TOKEN_VALUE_START "bar" TOKEN_VALUE_END LIST_END EOF' ); }); it('ordered lists with no space after the bullet are not parsed as a list', function() { var input = ` 1.bar `; var lex = Lexer(); var results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "1" TOKEN_VALUE_END WORDS TOKEN_VALUE_START ".bar" TOKEN_VALUE_END EOF' ); }); it('unordered lists with a space after the bullet are parsed as a list', function() { var input = ` - bar `; var lex = Lexer(); var results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'BREAK UNORDERED_LIST LIST_ITEM WORDS TOKEN_VALUE_START "bar" TOKEN_VALUE_END LIST_END EOF' ); }); it('unordered lists with no space after the bullet are not parsed as a list', function() { var input = ` -bar `; var lex = Lexer(); var results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "-bar" TOKEN_VALUE_END EOF' ); }); it('should handle a header', function() { var input = ` ## This is a header And this is a normal paragraph. [component]# This header is inside a component.[/component] `; var lex = Lexer(); var results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'BREAK HEADER_2 WORDS TOKEN_VALUE_START "This is a header" TOKEN_VALUE_END HEADER_END WORDS TOKEN_VALUE_START "And this is a normal paragraph." TOKEN_VALUE_END BREAK OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "component" TOKEN_VALUE_END CLOSE_BRACKET BREAK HEADER_1 WORDS TOKEN_VALUE_START "This header is inside a component." TOKEN_VALUE_END HEADER_END OPEN_BRACKET FORWARD_SLASH COMPONENT_NAME TOKEN_VALUE_START "component" TOKEN_VALUE_END CLOSE_BRACKET EOF' ); }); it('should handle numbers with leading decimals as prop values in components', function() { const input = `[component number:.1 /]`; const lex = Lexer(); const results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "component" TOKEN_VALUE_END COMPONENT_WORD TOKEN_VALUE_START "number" TOKEN_VALUE_END PARAM_SEPARATOR NUMBER TOKEN_VALUE_START ".1" TOKEN_VALUE_END FORWARD_SLASH CLOSE_BRACKET EOF' ); }); it('should reject numbers with multiple decimal points', function() { const input = `[component number:.1.1 /]`; const lex = Lexer(); expect(() => lex(input)).to.throwException(); }); it('should handle numbers with decimals as prop values in components', function() { const input = `[component number:1.1 /]`; const lex = Lexer(); const results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "component" TOKEN_VALUE_END COMPONENT_WORD TOKEN_VALUE_START "number" TOKEN_VALUE_END PARAM_SEPARATOR NUMBER TOKEN_VALUE_START "1.1" TOKEN_VALUE_END FORWARD_SLASH CLOSE_BRACKET EOF' ); }); it('should handle equation alias', function() { var lex = Lexer({}, { Eq: 'equation' }); var results = lex('[Eq]y = 0[/Eq]'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "equation" TOKEN_VALUE_END CLOSE_BRACKET WORDS TOKEN_VALUE_START "y = 0" TOKEN_VALUE_END OPEN_BRACKET FORWARD_SLASH COMPONENT_NAME TOKEN_VALUE_START "equation" TOKEN_VALUE_END CLOSE_BRACKET EOF' ); }); it('should handle code alias', function() { var lex = Lexer({}, { Cd: 'code' }); var results = lex('[Cd]y = 0[/Cd]'); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'OPEN_BRACKET COMPONENT_NAME TOKEN_VALUE_START "code" TOKEN_VALUE_END CLOSE_BRACKET WORDS TOKEN_VALUE_START "y = 0" TOKEN_VALUE_END OPEN_BRACKET FORWARD_SLASH COMPONENT_NAME TOKEN_VALUE_START "code" TOKEN_VALUE_END CLOSE_BRACKET EOF' ); }); it("symbols or numbers at end of paragraph doesn't remove paragraph break", function() { const input = ` Idyll is based on Markdown! Idyll posts are designed to support interaction and data-driven graphics. `; const lex = Lexer(); const results = lex(input); expect(results.tokens.flat(Number.POSITIVE_INFINITY).join(' ')).to.eql( 'WORDS TOKEN_VALUE_START "Idyll is based on Markdown" TOKEN_VALUE_END WORDS TOKEN_VALUE_START "!" TOKEN_VALUE_END BREAK WORDS TOKEN_VALUE_START "Idyll posts are designed to support interaction and\n data-driven graphics." TOKEN_VALUE_END EOF' ); }); }); describe('parser', function() { it('should parse a simple string', async function() { var input = 'Just a simple string'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['p', [], ['Just a simple string']]]] ]) ); }); it('should handle multiple blocks', async function() { var input = 'Just a simple string \n\n with some whitespace'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['p', [], ['Just a simple string ']], ['p', [], ['with some whitespace']] ] ] ]) ); }); it('should parse a closed var', async function() { var input = '[var /]'; expect(await compile(input)).to.eql(convertV1ToV2([['var', [], []]])); }); it('should parse a closed component', async function() { var input = '[var name:"v1" value:5 /]\n\nJust a simple string plus a component \n\n [VarDisplay var:v1 /]'; expect(await compile(input)).to.eql( convertV1ToV2([ ['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', async function() { var input = '[Slideshow currentSlide:1]test test test[/Slideshow]'; var output = await compile(input); }); it('should parse a nested component', async function() { var input = '[Slideshow currentSlide:1]text and stuff \n\n lots of newlines.\n\n[OpenComponent key:"val" ][/OpenComponent][/Slideshow]'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'Slideshow', [['currentSlide', ['value', 1]]], [ ['p', [], ['text and stuff ']], ['p', [], ['lots of newlines.']], ['OpenComponent', [['key', ['value', 'val']]], []] ] ] ] ] ]) ); }); it('should parse a nested component unambiguously', async function() { var input = '[Slideshow]text and stuff fewer of newlines.[OpenComponent][/OpenComponent][/Slideshow]'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'Slideshow', [], ['text and stuff fewer of newlines.', ['OpenComponent', [], []]] ] ] ] ]) ); }); it('should parse a simple nested component unambiguously', async function() { var input = '[Slideshow][OpenComponent][/OpenComponent][/Slideshow]'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [['Slideshow', [], [['OpenComponent', [], []]]]] ] ]) ); }); it('should handle an incline closed components with properties', async function() { var input = ` [meta title:"Compiler Test" description:"Short description of your project" /] `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'meta', [ ['title', ['value', 'Compiler Test']], [ 'description', ['value', 'Short description of your project'] ] ], [] ] ] ] ]) ); }); it('should parse an open component unambiguously', async function() { var input = '[Slideshow][/Slideshow]'; expect(await compile(input)).to.eql( convertV1ToV2([['TextContainer', [], [['Slideshow', [], []]]]]) ); }); it('should handle an inline closed component', async function() { var input = 'This is a normal text paragraph that [VarDisplay var:var /] has a component embedded in it.'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ 'This is a normal text paragraph that ', ['VarDisplay', [['var', ['variable', 'var']]], []], ' has a component embedded in it.' ] ] ] ] ]) ); }); it('should handle a header', async function() { var input = ` ## This is a header And this is a normal paragraph. This is # not a header. [component]# This header is inside a component.[/component] [component]This is not a # header inside a component.[/component] [component /] # Header End text `; console.log(await compile(input)); expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['h2', [], ['This is a header']], [ 'p', [], ['And this is a normal paragraph. This is # not a header.'] ], [ 'component', [], [['h1', [], ['This header is inside a component.']]] ], ['component', [], ['This is not a # header inside a component.']], ['component', [], []], ['h1', [], ['Header']], ['p', [], ['End text']] ] ] ]) ); }); it('should handle quotes', async function() { var input = ` > This is a quote And this is a normal paragraph. This is > not a quote. [component]> This quote is inside a component.[/component] [component]This is not a > quote inside a component.[/component] [component /] > quote End text `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['blockquote', [], ['This is a quote']], [ 'p', [], ['And this is a normal paragraph. This is > not a quote.'] ], [ 'component', [], [['blockquote', [], ['This quote is inside a component.']]] ], ['component', [], ['This is not a > quote inside a component.']], ['component', [], []], ['blockquote', [], ['quote']], ['p', [], ['End text']] ] ] ]) ); }); it('should ignore front-matter', async function() { var input = ` --- key: value title: Title --- ## This is a header And this is a normal paragraph. This is # not a header. [component]# This header is inside a component.[/component] [component]This is not a # header inside a component.[/component] [component /] # Header End text `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['h2', [], ['This is a header']], [ 'p', [], ['And this is a normal paragraph. This is # not a header.'] ], [ 'component', [], [['h1', [], ['This header is inside a component.']]] ], ['component', [], ['This is not a # header inside a component.']], ['component', [], []], ['h1', [], ['Header']], ['p', [], ['End text']] ] ] ]) ); }); it('should handle multiple headers', async function() { var input = ` # This is a header ## This is a header ### This is a header #### This is a header `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['h1', [], ['This is a header']], ['h2', [], ['This is a header']], ['h3', [], ['This is a header']], ['h4', [], ['This is a header']] ] ] ]) ); }); it('should handle a header that starts with a number', async function() { var input = ` # 1. This is a header ## 2. This is also a header ### 3. This too. `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['h1', [], ['1', '. This is a header']], ['h2', [], ['2', '. This is also a header']], ['h3', [], ['3', '. This too.']] ] ] ]) ); }); it('should parse an open component with a break at the end', async function() { var input = '[Slideshow currentSlide:1]text and stuff \n\n [/Slideshow]'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'Slideshow', [['currentSlide', ['value', 1]]], ['text and stuff '] ] ] ] ]) ); }); it('should parse a paragraph and code fence', async function() { var input = 'text text text lots of text\n\n\n```\nvar code = true;\n```\n'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['p', [], ['text text text lots of text']], ['pre', [], [['code', [], ['var code = true;']]]] ] ] ]) ); }); it('should parse a code fence with backticks inside', async function() { var input = 'text text text lots of text\n\n\n````\n```\nvar code = true;\n```\n````\n'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['p', [], ['text text text lots of text']], ['pre', [], [['code', [], ['```\nvar code = true;\n```']]]] ] ] ]) ); }); it('should parse inline code with backticks inside', async function() { var input = 'text text text lots of text `` `var code = true;` ``\n'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ 'text text text lots of text ', ['code', [], ['`var code = true;`']] ] ] ] ] ]) ); }); it('should handle backticks in a paragraph', async function() { var input = 'regular text and stuff, then some `code`'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], ['regular text and stuff, then some ', ['code', [], ['code']]] ] ] ] ]) ); }); it('should ignore comments', async function() { var input = ` Text. / Not a comment. // Comment // Second comment [component]//not a comment // comment inside components [/component]// is a comment not a comment: https://stuff.com `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['p', [], ['Text. / Not a comment.\n ']], ['component', [], ['//not a comment\n ']], [ 'p', [], [ [ 'span', [], [ 'not a comment: ', [ 'a', [['href', ['value', 'https://stuff.com']]], ['https://stuff.com'] ] ] ] ] ] ] ] ]) ); }); it('should accept negative numbers', async function() { var input = '[component prop:-10 /]'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['component', [['prop', ['value', -10]]], []]]] ]) ); }); it('should accept positive numbers', async function() { var input = '[component prop:10 /]'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['component', [['prop', ['value', 10]]], []]]] ]) ); }); it('should accept numbers /w a leading decimal point', async function() { const input = '[component prop:.1 /]'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['component', [['prop', ['value', 0.1]]], []]]] ]) ); }); it('should handle booleans', async function() { const input = '[component prop:true /]'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [['component', [['prop', ['value', true]]], []]] ] ]) ); }); it('should handle booleans in backticks', async function() { const input = '[component prop:`true` /]'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [['component', [['prop', ['expression', 'true']]], []]] ] ]) ); }); it('should handle italics and bold', async function() { const input = 'regular text and stuff, then some *italics* and some **bold**.'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ 'regular text and stuff, then some ', ['em', [], ['italics']], ' and some ', ['strong', [], ['bold']], '.' ] ] ] ] ]) ); }); it('should handle unordered list', async function() { const input = '* this is the first unordered list item\n* this is the second unordered list item'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'ul', [], [ ['li', [], ['this is the first unordered list item']], ['li', [], ['this is the second unordered list item']] ] ] ] ] ]) ); }); it('should handle ordered list', async function() { const input = '1. this is the first ordered list item\n2. this is the second ordered list item'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'ol', [], [ ['li', [], ['this is the first ordered list item']], ['li', [], ['this is the second ordered list item']] ] ] ] ] ]) ); }); it('should handle inline links', async 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(await compile(input)).to.eql( convertV1ToV2([ [ '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.' ] ] ] ] ]) ); }); it('should handle inline images', async 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(await compile(input)).to.eql( convertV1ToV2([ [ '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.' ] ] ] ] ]) ); }); it('should handle lines that start with bold or italic', async function() { const input = '**If** I start a line with bold this should work,\nwhat if I\n\n*start with an italic*?'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ ['strong', [], ['If']], ' I start a line with bold this should work,\nwhat if I' ] ], ['p', [], [['em', [], ['start with an italic']], '?']] ] ] ]) ); }); it('should handle component name with a period', async function() { const input = 'This component name has a period separator [component.val /].'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ 'This component name has a period separator ', ['component.val', [], []], '.' ] ] ] ] ]) ); }); it('should handle component name with multiple periods', async function() { const input = 'This component name has a period separator [component.val.v /].'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ 'This component name has a period separator ', ['component.val.v', [], []], '.' ] ] ] ] ]) ); }); it('should handle strong text with a p', async function() { const input = '**p a**'; expect(await compile(input)).to.eql( convertV1ToV2([['TextContainer', [], [['strong', [], ['p a']]]]]) ); }); it('should handle strong emphasized text using asterisks', async function() { const input = '***test***'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['strong', [], [['em', [], ['test']]]]]] ]) ); }); it('should handle strong emphasized text using underscores', async function() { const input = '___test___'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['strong', [], [['em', [], ['test']]]]]] ]) ); }); it('should handle strong emphasized text using mixed asterisks and underscores - 1', async function() { const input = '_**test**_'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['em', [], [['strong', [], ['test']]]]]] ]) ); }); it('should handle strong emphasized text using mixed asterisks and underscores - 2', async function() { const input = '**_test_**'; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['strong', [], [['em', [], ['test']]]]]] ]) ); }); it('should merge consecutive word blocks', async function() { const input = '[Equation]y = 0[/Equation]'; expect(await compile(input)).to.eql( convertV1ToV2([['TextContainer', [], [['equation', [], ['y = 0']]]]]) ); }); it('should not put smartquotes in code blocks', async function() { const input = "`Why 'hello' there`"; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['code', [], ["Why 'hello' there"]]]] ]) ); }); it('should handle a language in a codeblock ', async function() { const input = '```json\n{}\n```'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [['CodeHighlight', [['language', ['value', 'json']]], ['{}']]] ] ]) ); }); it('should handle an i tag', async function() { const input = '[i]not even em[/i]'; expect(await compile(input)).to.eql( convertV1ToV2([['TextContainer', [], [['i', [], ['not even em']]]]]) ); }); it('should not insert extra div tags', async function() { const input = ` [Slideshow] [Slide/] [Slide/] [Slide/] [/Slideshow]`; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'Slideshow', [], [['Slide', [], []], ['Slide', [], []], ['Slide', [], []]] ] ] ] ]) ); }); it('should handle items nested in a header', async function() { const input = `# My header is **bold**!`; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [['h1', [], ['My header is ', ['strong', [], ['bold']], '!']]] ] ]) ); }); it('should handle full width components and elements', async function() { const input = ` This is text [FullWidth] This is full width [/FullWidth] [div fullWidth:true /] This is not full width `; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['p', [], ['This is text']]]], [ 'div', [['className', ['value', 'fullWidth']]], ['\n This is full width\n '] ], ['div', [], []], ['TextContainer', [], [['p', [], ['This is not full width']]]] ]) ); }); it('should handle lists followed by emphasised text', async function() { const input = ` - foo - bar *Wow!* `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['ul', [], [['li', [], ['foo']], ['li', [], ['bar']]]], ['em', [], ['Wow', '!']] ] ] ]) ); }); it('should preserve space between inline blocks - 1', async function() { const input = `*text* __other text__`; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [['em', [], ['text']], ' ', ['strong', [], ['other text']]] ] ] ] ]) ); }); it('should preserve space between inline blocks - 2', async function() { const input = `[em]text[/em] [b]other text[/b]`; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [['p', [], [['em', [], ['text']], ' ', ['b', [], ['other text']]]]] ] ]) ); }); it('should handle equations with strange things inside - 1', async function() { const input = `[equation display:true]\sum_{j=0}^n x^{j} + \sum x^{k}[/equation]`; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'equation', [['display', ['value', true]]], ['sum_{j=0}^n x^{j} + sum x^{k}'] ] ] ] ]) ); }); it('should handle equations with strange things inside - 2', async function() { const input = ` [equation display:true]\sum_{j=0}^n x^{j} + \sum_{k=0}^n x^{k} [/equation] `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'equation', [['display', ['value', true]]], ['sum_{j=0}^n x^{j} + sum_{k=0}^n x^{k}'] ] ] ] ]) ); }); it('should handle code blocks with parens inside', async function() { const input = `[code](n - 1)!/2 possible paths[/code]`; expect(await compile(input)).to.eql( convertV1ToV2([ ['TextContainer', [], [['code', [], ['(n - 1)!/2 possible paths']]]] ]) ); }); it('should respect linebreaks', async function() { const input = ` How many lines should this text be on ? `; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ ['p', [], ['How many\n lines should']], ['p', [], ['this text\n be\n on\n ?']] ] ] ]) ); }); }); describe('error handling', function() { it('record line and column number of an error', async function() { const input = 'This string contains an un-closed component [BadComponent key:"val" ] '; try { const output = await compile(input); } 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', async function() { const input = 'Hello World'; const output = await compile(input, { plugins: [ast => appendTextNode(ast, ':)')] }); expect(output).to.eql( convertV1ToV2([ ['TextContainer', [], [['p', [], ['Hello World']]]], ['TextContainer', [], [':)']] ]) ); }); it('should handle an asynchronous post-processing plugin', async function() { const input = 'Hello World'; const output = await compile(input, { plugins: [ast => Promise.resolve(appendTextNode(ast, ':)'))] }); expect(output).to.eql( convertV1ToV2([ ['TextContainer', [], [['p', [], ['Hello World']]]], ['TextContainer', [], [':)']] ]) ); }); it('should handle multiple synchronous post-processing plugins', async function() { const input = 'Hello World'; const output = await compile(input, { plugins: [ ast => appendTextNode(ast, ':)'), ast => appendTextNode(ast, ':(') ] }); expect(output).to.eql( convertV1ToV2([ ['TextContainer', [], [['p', [], ['Hello World']]]], ['TextContainer', [], [':)']], ['TextContainer', [], [':(']] ]) ); }); it('should handle multiple asynchronous post-processing plugins', async function() { const input = 'Hello World'; const output = await compile(input, { plugins: [ ast => Promise.resolve(appendTextNode(ast, ':)')), ast => Promise.resolve(appendTextNode(ast, ':(')) ] }); expect(output).to.eql( convertV1ToV2([ ['TextContainer', [], [['p', [], ['Hello World']]]], ['TextContainer', [], [':)']], ['TextContainer', [], [':(']] ]) ); }); it('should handle mixed synchronous and asynchronous post-processing plugins', async function() { const input = 'Hello World'; const output = await compile(input, { plugins: [ ast => Promise.resolve(appendTextNode(ast, '1')), ast => appendTextNode(ast, '2'), ast => Promise.resolve(appendTextNode(ast, '3')), ast => appendTextNode(ast, '4') ] }); expect(output).to.eql( convertV1ToV2([ ['TextContainer', [], [['p', [], ['Hello World']]]], ['TextContainer', [], ['1']], ['TextContainer', [], ['2']], ['TextContainer', [], ['3']], ['TextContainer', [], ['4']] ]) ); }); it('should handle a link', async function() { const input = 'https://www.google.com/'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ [ 'span', [], [ [ 'a', [['href', ['value', 'https://www.google.com/']]], ['https://www.google.com/'] ] ] ] ] ] ] ] ]) ); }); it('should handle one link in text', async function() { const input = 'Here is a link to website https://www.google.com/'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ [ 'span', [], [ 'Here is a link to website ', [ 'a', [['href', ['value', 'https://www.google.com/']]], ['https://www.google.com/'] ] ] ] ] ] ] ] ]) ); }); it('should handle one links in between text', async function() { const input = 'Here is a link to website https://www.google.com/ Click here!'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ [ 'span', [], [ 'Here is a link to website ', [ 'a', [['href', ['value', 'https://www.google.com/']]], ['https://www.google.com/'] ], ' Click here!' ] ] ] ] ] ] ]) ); }); it('should handle two links in between text', async function() { const input = 'Here is a link to website https://www.google.com/ Click here! https://www.go.com/'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ [ 'span', [], [ 'Here is a link to website ', [ 'a', [['href', ['value', 'https://www.google.com/']]], ['https://www.google.com/'] ], ' Click here! ', [ 'a', [['href', ['value', 'https://www.go.com/']]], ['https://www.go.com/'] ] ] ] ] ] ] ] ]) ); }); it('should handle a link before any text', async function() { const input = 'https://www.google.com/ . Hello World'; expect(await compile(input)).to.eql( convertV1ToV2([ [ 'TextContainer', [], [ [ 'p', [], [ [ 'span', [], [ [ 'a', [['href', ['value', 'https://www.google.com/']]], ['https://www.google.com/'] ], ' . Hello World' ] ] ] ] ] ] ]) ); }); it('should handle bold at the end of a paragraph', async function() { const input = ` This is **bold text.** This is a new paragraph. `; expect(await compile(input)).to.eql({ type: 'component', name: 'div', children: [ { type: 'component', name: 'TextContainer', children: [ { type: 'component', name: 'p', children: [ { type: 'textnode', value: 'This is ' }, { type: 'component', name: 'strong', children: [{ type: 'textnode', value: 'bold text.' }] } ] }, { type: 'component', name: 'p', children: [ { type: 'textnode', value: 'This is a new paragraph.' } ] } ] } ] }); }); }); it('should handle numbers and symbols with and without spaces', async fun