Skip to content

Commit

Permalink
feat: inline comment tag (#514)
Browse files Browse the repository at this point in the history
* style: remove unnecessary intermediate constant

* feat: add inline comment tag

* refactor: use readIdentifier when reading tag names

* docs: add inline comment tag
  • Loading branch information
jg-rp committed Jul 7, 2022
1 parent 4a0186d commit 2f87708
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/source/_data/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ filters:

tags:
overview: overview.html
"# (inline comment)": inline_comment.html
assign: assign.html
capture: capture.html
case: case.html
Expand Down
50 changes: 50 additions & 0 deletions docs/source/tags/inline_comment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: "# (inline comment)"
---

{% since %}v9.38.0{% endsince %}

Add comments to a Liquid template using an inline tag. Text enclosed in an inline comment tag will not be printed.

Input
```liquid
Anything inside an inline comment tag will not be printed.
{% # this is an inline comment %}
But every line must start with a '#'.
{%
# this is a comment
# that spans multiple lines
%}
```

Output
```text
Anything inside an inline comment tag will not be printed.
But every line must start with a '#'.
```

Inline comments are useful inside <a href="./liquid.html">`liquid`</a> tags too.

```liquid
{% liquid
# required args
assign product = collection.products.first
# optional args
assign should_show_border = should_show_border | default: true
assign should_highlight = should_highlight | default: false
%}
```

But they don't work well for commenting out blocks of Liquid code. The <a href="./comment.html">`comment`</a> block tag is the better option when you need to temporarily stop other tags from being executed.

Input
```liquid
{%- # {% echo 'Welcome to LiquidJS!' %} -%}
{% comment %}{% echo 'Welcome to LiquidJS!' %}{% endcomment %}
```

Output
```text
-%}
```
3 changes: 2 additions & 1 deletion src/builtin/tags/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import Break from './break'
import Continue from './continue'
import echo from './echo'
import liquid from './liquid'
import inlineComment from './inline-comment'
import { TagImplOptions } from '../../template/tag/tag-impl-options'

const tags: { [key: string]: TagImplOptions } = {
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue, echo, liquid
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue, echo, liquid, '#': inlineComment
}

export default tags
11 changes: 11 additions & 0 deletions src/builtin/tags/inline-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { TagToken } from '../../tokens/tag-token'
import { TopLevelToken } from '../../tokens/toplevel-token'
import { TagImplOptions } from '../../template/tag/tag-impl-options'

export default {
parse: function (tagToken: TagToken, remainTokens: TopLevelToken[]) {
if (tagToken.args.search(/\n\s*[^#\s]/g) !== -1) {
throw new Error('every line of an inline comment must start with a \'#\' character')
}
}
} as TagImplOptions
10 changes: 8 additions & 2 deletions src/parser/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,7 @@ export class Tokenizer {
const begin = this.p
let end = this.N
if (this.readToDelimiter('\n') !== -1) end = this.p
const token = new LiquidTagToken(input, begin, end, options, file)
return token
return new LiquidTagToken(input, begin, end, options, file)
}

mkError (msg: string, begin: number) {
Expand All @@ -234,6 +233,13 @@ export class Tokenizer {
return new IdentifierToken(this.input, begin, this.p, this.file)
}

readTagName (): string {
this.skipBlank()
// Handle inline comment tags
if (this.input[this.p] === '#') return this.input.slice(this.p, ++this.p)
return this.readIdentifier().getText()
}

readHashes (jekyllStyle?: boolean) {
const hashes = []
while (true) {
Expand Down
2 changes: 1 addition & 1 deletion src/tokens/liquid-tag-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export class LiquidTagToken extends DelimitedToken {
this.args = ''
} else {
const tokenizer = new Tokenizer(this.content, options.operatorsTrie)
this.name = tokenizer.readIdentifier().getText()
this.name = tokenizer.readTagName()
if (!this.name) throw new TokenizationError(`illegal liquid tag syntax`, this)

tokenizer.skipBlank()
Expand Down
2 changes: 1 addition & 1 deletion src/tokens/tag-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class TagToken extends DelimitedToken {
super(TokenKind.Tag, value, input, begin, end, trimTagLeft, trimTagRight, file)

const tokenizer = new Tokenizer(this.content, options.operatorsTrie)
this.name = tokenizer.readIdentifier().getText()
this.name = tokenizer.readTagName()
if (!this.name) throw new TokenizationError(`illegal tag syntax`, this)

tokenizer.skipBlank()
Expand Down
95 changes: 95 additions & 0 deletions test/integration/builtin/tags/inline-comment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Liquid } from '../../../../src/liquid'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'

use(chaiAsPromised)

describe('tags/inline-comment', function () {
const liquid = new Liquid()
it('should ignore plain string', async function () {
const src = 'My name is {% # super %} Shopify.'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('My name is Shopify.')
})
it('should ignore output tokens', async function () {
const src = '{% #\n{{ foo}} \n %}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should support whitespace control', async function () {
const src = '{%- # some comment \n -%}\nfoo'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('foo')
})
it('should handle hash without trailing whitespace', async function () {
const src = '{% #some comment %}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should handle hash without leading whitespace', async function () {
const src = '{%#some comment %}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should handle empty comment', async function () {
const src = '{%#%}'
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should support multiple lines', async function () {
const src = [
'{%-',
' # spread inline comments',
' # over multiple lines',
'-%}'
].join('\n')
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
it('should enforce leading hashes', async function () {
const src = [
'{%-',
' # spread inline comments',
' over multiple lines',
'-%}'
].join('\n')
return expect(liquid.parseAndRender(src))
.to.be.rejectedWith(/every line of an inline comment must start with a '#' character/)
})
describe('sync support', function () {
it('should ignore plain string', function () {
const src = 'My name is {% # super %} Shopify.'
const html = liquid.parseAndRenderSync(src)
return expect(html).to.equal('My name is Shopify.')
})
})
describe('liquid tag', function () {
it('should treat lines starting with a hash as a comment', async function () {
const src = [
'{% liquid ',
' # first comment line',
' # second comment line',
'',
' # another comment line',
' echo \'Hello \'',
'',
' # more comments',
' echo \'goodbye\'',
'-%}'
].join('\n')
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('Hello goodbye')
})
it('should handle lots of hashes', async function () {
const src = [
'{% liquid',
' ##########################',
' # spread inline comments #',
' ##########################',
'-%}'
].join('\n')
const html = await liquid.parseAndRender(src)
return expect(html).to.equal('')
})
})
})
29 changes: 29 additions & 0 deletions test/unit/parser/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,4 +525,33 @@ describe('Tokenizer', function () {
expect(() => tokenizer.readLiquidTagTokens()).to.throw(/illegal liquid tag syntax/)
})
})
describe('#read inline comment tags', () => {
it('should allow hash characters in tag names', () => {
const tokenizer = new Tokenizer('{% # some comment %}', trie)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(1)
const tag = tokens[0] as TagToken
expect(tag).instanceOf(TagToken)
expect(tag.name).to.equal('#')
expect(tag.args).to.equal('some comment')
})
it('should handle leading whitespace', () => {
const tokenizer = new Tokenizer('{%\n # some comment %}', trie)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(1)
const tag = tokens[0] as TagToken
expect(tag).instanceOf(TagToken)
expect(tag.name).to.equal('#')
expect(tag.args).to.equal('some comment')
})
it('should handle no trailing whitespace', () => {
const tokenizer = new Tokenizer('{%\n #some comment %}', trie)
const tokens = tokenizer.readTopLevelTokens()
expect(tokens.length).to.equal(1)
const tag = tokens[0] as TagToken
expect(tag).instanceOf(TagToken)
expect(tag.name).to.equal('#')
expect(tag.args).to.equal('some comment')
})
})
})

0 comments on commit 2f87708

Please sign in to comment.