Added code blocks and improved escape handling

This commit is contained in:
Ben Ashton 2022-09-25 16:00:47 -06:00
parent e23382f729
commit 98c6c442fb
3 changed files with 69 additions and 19 deletions

View File

@ -0,0 +1,5 @@
<h1>Hello</h1>
${
echo "hello world";
echo "\}$";
}$

View File

@ -1,5 +1,15 @@
import { TokenStream } from './token_stream.mjs'; import { TokenStream } from './token_stream.mjs';
const htmlEscapeSed = `sed '` +
`s/\\&/\\&amp;/g;` +
`s/</\\&lt;/g;` +
`s/>/\\&gt;/g;` +
`s/"/\\&quot;/g;` +
`s/'\\''/\\&#39;/g` +
`'`;
export class TemplateEngine { export class TemplateEngine {
async render(fileName) { async render(fileName) {
const ts = new TokenStream(); const ts = new TokenStream();
@ -14,6 +24,9 @@ export class TemplateEngine {
case 'raw': case 'raw':
buffer += this._renderRaw(token.value); buffer += this._renderRaw(token.value);
break; break;
case 'block_statement':
buffer += this._renderBlockStatement(token.value);
break;
case 'inline': case 'inline':
buffer += this._renderInline(token.value); buffer += this._renderInline(token.value);
break; break;
@ -40,6 +53,10 @@ export class TemplateEngine {
return buffer; return buffer;
} }
_renderBlockStatement(value) {
return value.replace(/\s*$/, '\n');
}
_renderComment(value) { _renderComment(value) {
return `# ${value}\n`; return `# ${value}\n`;
} }
@ -49,7 +66,7 @@ export class TemplateEngine {
} }
_renderInline(value) { _renderInline(value) {
return `printf '%s' "${value}" | jq -Rr @html | head -c -1;\n`; return `printf '%s' "${value}" | ${htmlEscapeSed}\n`;
} }
_renderInlineUnescaped(value) { _renderInlineUnescaped(value) {
@ -57,7 +74,7 @@ export class TemplateEngine {
} }
_renderInlineStatement(value) { _renderInlineStatement(value) {
return `printf '%s' "$(${value})" | jq -Rr @html | head -c -1;\n`; return `printf '%s' "$(${value})" | ${htmlEscapeSed}\n`;
} }
_renderInlineStatementUnescaped(value) { _renderInlineStatementUnescaped(value) {

View File

@ -3,6 +3,8 @@ import { TemplateSyntaxError } from './errors/template_syntax_error.mjs';
export class TokenStream { export class TokenStream {
static ESCAPE = '\\'; static ESCAPE = '\\';
static OPEN_STATEMENT = '${';
static CLOSE_STATEMENT = '}$';
static OPEN_INLINE = '<%'; static OPEN_INLINE = '<%';
static CLOSE_INLINE = '%>'; static CLOSE_INLINE = '%>';
static OPEN_INLINE_UNESCAPED = '<!%'; static OPEN_INLINE_UNESCAPED = '<!%';
@ -64,10 +66,12 @@ export class TokenStream {
// interpreter that first builds the template and then executes) // interpreter that first builds the template and then executes)
this._skipShebang(); this._skipShebang();
} else if (this._cs.isNext(TokenStream.ESCAPE)) { } else if (this._cs.isNext(TokenStream.ESCAPE)) {
// Skip space // Skip escape
this._cs.next(); this._cs.next(TokenStream.ESCAPE.length);
// Treat everything as raw until next space // Treat everything as raw until next space
raw += this._cs.nextWhile((c) => /\S/.test(c)); raw += this._cs.nextWhile((c) => /\S/.test(c));
} else if (this._cs.isNext(TokenStream.OPEN_STATEMENT)) {
return flushRaw() || this._readBlockStatement();
} else if (this._cs.isNext(TokenStream.OPEN_INLINE)) { } else if (this._cs.isNext(TokenStream.OPEN_INLINE)) {
return flushRaw() || this._readInline(); return flushRaw() || this._readInline();
} else if (this._cs.isNext(TokenStream.OPEN_INLINE_UNESCAPED)) { } else if (this._cs.isNext(TokenStream.OPEN_INLINE_UNESCAPED)) {
@ -102,40 +106,64 @@ export class TokenStream {
this._cs.next(); this._cs.next();
} }
_missingInlineSpaceError(tag) { _missingOpenSpaceError(tag) {
throw new TemplateSyntaxError( throw new TemplateSyntaxError(
this._cs, 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); this._cs.next(openTag.length);
// Check for mandatory space // Check for mandatory space
if (this._cs.peek() !== ' ') { if (/\S/.test(this._cs.peek())) {
this._missingInlineSpaceError(openTag); this._missingOpenSpaceError(openTag);
} }
this._cs.next(); this._cs.next();
let value = ''; let value = '';
while (!this._cs.eof() && !this._cs.isNext(closeTag)) { 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)) { // Treat everything as raw until next space or eof
if (char !== ' ') { value += this._cs.nextWhile((c) => /\S/.test(c));
this._missingInlineSpaceError(closeTag);
}
} else { } 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); this._cs.next(closeTag.length);
return {type: tokenType, value}; return {type: tokenType, value};
} }
_readBlockStatement() {
return this._readBlockGeneric(
'block_statement',
TokenStream.OPEN_STATEMENT,
TokenStream.CLOSE_STATEMENT
);
}
_readInline() { _readInline() {
return this._readInlineGeneric( return this._readBlockGeneric(
'inline', 'inline',
TokenStream.OPEN_INLINE, TokenStream.OPEN_INLINE,
TokenStream.CLOSE_INLINE TokenStream.CLOSE_INLINE
@ -143,7 +171,7 @@ export class TokenStream {
} }
_readInlineUnescaped() { _readInlineUnescaped() {
return this._readInlineGeneric( return this._readBlockGeneric(
'inline_unescaped', 'inline_unescaped',
TokenStream.OPEN_INLINE_UNESCAPED, TokenStream.OPEN_INLINE_UNESCAPED,
TokenStream.CLOSE_INLINE_UNESCAPED TokenStream.CLOSE_INLINE_UNESCAPED
@ -151,7 +179,7 @@ export class TokenStream {
} }
_readInlineStatement() { _readInlineStatement() {
return this._readInlineGeneric( return this._readBlockGeneric(
'inline_statement', 'inline_statement',
TokenStream.OPEN_INLINE_STATEMENT, TokenStream.OPEN_INLINE_STATEMENT,
TokenStream.CLOSE_INLINE_STATEMENT TokenStream.CLOSE_INLINE_STATEMENT
@ -159,7 +187,7 @@ export class TokenStream {
} }
_readInlineStatementUnescaped() { _readInlineStatementUnescaped() {
return this._readInlineGeneric( return this._readBlockGeneric(
'inline_statement_unescaped', 'inline_statement_unescaped',
TokenStream.OPEN_INLINE_STATEMENT_UNESCAPED, TokenStream.OPEN_INLINE_STATEMENT_UNESCAPED,
TokenStream.CLOSE_INLINE_STATEMENT_UNESCAPED TokenStream.CLOSE_INLINE_STATEMENT_UNESCAPED