diff --git a/example_templates/code_block.n0m b/example_templates/code_block.n0m new file mode 100644 index 0000000..bcacfaf --- /dev/null +++ b/example_templates/code_block.n0m @@ -0,0 +1,5 @@ +

Hello

+${ + echo "hello world"; + echo "\}$"; +}$ diff --git a/src/template_engine.mjs b/src/template_engine.mjs index 4fe5246..ef55b95 100644 --- a/src/template_engine.mjs +++ b/src/template_engine.mjs @@ -1,5 +1,15 @@ import { TokenStream } from './token_stream.mjs'; + +const htmlEscapeSed = `sed '` + + `s/\\&/\\&/g;` + + `s//\\>/g;` + + `s/"/\\"/g;` + + `s/'\\''/\\'/g` + + `'`; + + export class TemplateEngine { async render(fileName) { const ts = new TokenStream(); @@ -14,6 +24,9 @@ export class TemplateEngine { case 'raw': buffer += this._renderRaw(token.value); break; + case 'block_statement': + buffer += this._renderBlockStatement(token.value); + break; case 'inline': buffer += this._renderInline(token.value); break; @@ -40,6 +53,10 @@ export class TemplateEngine { return buffer; } + _renderBlockStatement(value) { + return value.replace(/\s*$/, '\n'); + } + _renderComment(value) { return `# ${value}\n`; } @@ -49,7 +66,7 @@ export class TemplateEngine { } _renderInline(value) { - return `printf '%s' "${value}" | jq -Rr @html | head -c -1;\n`; + return `printf '%s' "${value}" | ${htmlEscapeSed}\n`; } _renderInlineUnescaped(value) { @@ -57,7 +74,7 @@ export class TemplateEngine { } _renderInlineStatement(value) { - return `printf '%s' "$(${value})" | jq -Rr @html | head -c -1;\n`; + return `printf '%s' "$(${value})" | ${htmlEscapeSed}\n`; } _renderInlineStatementUnescaped(value) { diff --git a/src/token_stream.mjs b/src/token_stream.mjs index 4318979..e76a074 100644 --- a/src/token_stream.mjs +++ b/src/token_stream.mjs @@ -3,6 +3,8 @@ import { TemplateSyntaxError } from './errors/template_syntax_error.mjs'; export class TokenStream { static ESCAPE = '\\'; + static OPEN_STATEMENT = '${'; + static CLOSE_STATEMENT = '}$'; static OPEN_INLINE = '<%'; static CLOSE_INLINE = '%>'; static OPEN_INLINE_UNESCAPED = ' /\S/.test(c)); + } else if (this._cs.isNext(TokenStream.OPEN_STATEMENT)) { + return flushRaw() || this._readBlockStatement(); } else if (this._cs.isNext(TokenStream.OPEN_INLINE)) { return flushRaw() || this._readInline(); } else if (this._cs.isNext(TokenStream.OPEN_INLINE_UNESCAPED)) { @@ -102,40 +106,64 @@ export class TokenStream { this._cs.next(); } - _missingInlineSpaceError(tag) { + _missingOpenSpaceError(tag) { throw new TemplateSyntaxError( this._cs, - `Inline tag: "${tag}" must be followed by a whitespace` + `Tag: "${tag}" must be followed by a whitespace or new line` ); } - _readInlineGeneric(tokenType, openTag, closeTag) { + _missingCloseSpaceError(tag) { + throw new TemplateSyntaxError( + this._cs, + `Tag: "${tag}" must be preceded by a whitespace or new line` + ); + } + + _readBlockGeneric(tokenType, openTag, closeTag) { this._cs.next(openTag.length); // Check for mandatory space - if (this._cs.peek() !== ' ') { - this._missingInlineSpaceError(openTag); + if (/\S/.test(this._cs.peek())) { + this._missingOpenSpaceError(openTag); } this._cs.next(); let value = ''; while (!this._cs.eof() && !this._cs.isNext(closeTag)) { - const char = this._cs.next(); + // Handle any escaped closing tag + if (this._cs.isNext(TokenStream.ESCAPE)) { + // Skip escape + this._cs.next(TokenStream.ESCAPE.length); - if (this._cs.isNext(closeTag)) { - if (char !== ' ') { - this._missingInlineSpaceError(closeTag); - } + // Treat everything as raw until next space or eof + value += this._cs.nextWhile((c) => /\S/.test(c)); } else { - value += char; + const char = this._cs.next(); + + if (this._cs.isNext(closeTag)) { + if (/\S/.test(char)) { + this._missingCloseSpaceError(closeTag); + } + } else { + value += char; + } } } this._cs.next(closeTag.length); return {type: tokenType, value}; } + _readBlockStatement() { + return this._readBlockGeneric( + 'block_statement', + TokenStream.OPEN_STATEMENT, + TokenStream.CLOSE_STATEMENT + ); + } + _readInline() { - return this._readInlineGeneric( + return this._readBlockGeneric( 'inline', TokenStream.OPEN_INLINE, TokenStream.CLOSE_INLINE @@ -143,7 +171,7 @@ export class TokenStream { } _readInlineUnescaped() { - return this._readInlineGeneric( + return this._readBlockGeneric( 'inline_unescaped', TokenStream.OPEN_INLINE_UNESCAPED, TokenStream.CLOSE_INLINE_UNESCAPED @@ -151,7 +179,7 @@ export class TokenStream { } _readInlineStatement() { - return this._readInlineGeneric( + return this._readBlockGeneric( 'inline_statement', TokenStream.OPEN_INLINE_STATEMENT, TokenStream.CLOSE_INLINE_STATEMENT @@ -159,7 +187,7 @@ export class TokenStream { } _readInlineStatementUnescaped() { - return this._readInlineGeneric( + return this._readBlockGeneric( 'inline_statement_unescaped', TokenStream.OPEN_INLINE_STATEMENT_UNESCAPED, TokenStream.CLOSE_INLINE_STATEMENT_UNESCAPED