diff --git a/docs/source/_data/sidebar.yml b/docs/source/_data/sidebar.yml
index f8eaf46c96..d697eaa0c2 100644
--- a/docs/source/_data/sidebar.yml
+++ b/docs/source/_data/sidebar.yml
@@ -78,6 +78,7 @@ filters:
tags:
overview: overview.html
+ "# (inline comment)": inline_comment.html
assign: assign.html
capture: capture.html
case: case.html
diff --git a/docs/source/tags/inline_comment.md b/docs/source/tags/inline_comment.md
new file mode 100644
index 0000000000..e40f0eb41a
--- /dev/null
+++ b/docs/source/tags/inline_comment.md
@@ -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 `liquid` 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 `comment` 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
+ -%}
+```
diff --git a/src/builtin/tags/index.ts b/src/builtin/tags/index.ts
index 7951ea8c91..82ec5b4a49 100644
--- a/src/builtin/tags/index.ts
+++ b/src/builtin/tags/index.ts
@@ -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
diff --git a/src/builtin/tags/inline-comment.ts b/src/builtin/tags/inline-comment.ts
new file mode 100644
index 0000000000..755d4ca3f2
--- /dev/null
+++ b/src/builtin/tags/inline-comment.ts
@@ -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
diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts
index 621f9000e6..7f02313ab0 100644
--- a/src/parser/tokenizer.ts
+++ b/src/parser/tokenizer.ts
@@ -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) {
@@ -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) {
diff --git a/src/tokens/liquid-tag-token.ts b/src/tokens/liquid-tag-token.ts
index d3681e1bc6..e8a0f6fa72 100644
--- a/src/tokens/liquid-tag-token.ts
+++ b/src/tokens/liquid-tag-token.ts
@@ -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()
diff --git a/src/tokens/tag-token.ts b/src/tokens/tag-token.ts
index 6163a9e1be..b8ebecce0c 100644
--- a/src/tokens/tag-token.ts
+++ b/src/tokens/tag-token.ts
@@ -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()
diff --git a/test/integration/builtin/tags/inline-comment.ts b/test/integration/builtin/tags/inline-comment.ts
new file mode 100644
index 0000000000..08d7ef4478
--- /dev/null
+++ b/test/integration/builtin/tags/inline-comment.ts
@@ -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('')
+ })
+ })
+})
diff --git a/test/unit/parser/tokenizer.ts b/test/unit/parser/tokenizer.ts
index 2acf7efbcf..8ec6b0b945 100644
--- a/test/unit/parser/tokenizer.ts
+++ b/test/unit/parser/tokenizer.ts
@@ -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')
+ })
+ })
})