0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 18:32:41 +00:00

Merge pull request #2774 from Gazook89/mustache-syntax-unit-tests

Mustache syntax unit tests
This commit is contained in:
Trevor Buckner
2023-04-07 14:10:53 -04:00
committed by GitHub
5 changed files with 396 additions and 158 deletions

View File

@@ -63,7 +63,7 @@ jobs:
command: npm run test:basic
- run:
name: Test - Mustache Spans
command: npm run test:mustache-span
command: npm run test:mustache-syntax
- run:
name: Test - Routes
command: npm run test:route

View File

@@ -24,6 +24,7 @@ module.exports = {
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
'react/jsx-uses-react' : 'error',
'react/prefer-es6-class' : ['error', 'never'],
'jest/valid-expect' : ['error', { maxArgs: 2 }],
/** Warnings **/
'max-lines' : ['warn', {

View File

@@ -23,7 +23,10 @@
"test:coverage": "jest --coverage --silent --runInBand",
"test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --verbose --noStackTrace",
"test:mustache-syntax:inline": "jest '.*(mustache-syntax).*' -t '^Inline:.*' --verbose --noStackTrace",
"test:mustache-syntax:block": "jest '.*(mustache-syntax).*' -t '^Block:.*' --verbose --noStackTrace",
"test:mustache-syntax:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build",
@@ -45,20 +48,21 @@
"coveragePathIgnorePatterns": [
"build/*"
],
"coverageThreshold" : {
"global" : {
"statements" : 25,
"branches" : 10,
"functions" : 22,
"lines" : 25
"coverageThreshold": {
"global": {
"statements": 25,
"branches": 10,
"functions": 22,
"lines": 25
},
"server/homebrew.api.js" : {
"statements" : 65,
"branches" : 50,
"functions" : 60,
"lines" : 70
"server/homebrew.api.js": {
"statements": 65,
"branches": 50,
"functions": 60,
"lines": 70
}
}
},
"setupFilesAfterEnv": ["jest-expect-message"]
},
"babel": {
"presets": [
@@ -109,6 +113,7 @@
"eslint": "^8.37.0",
"eslint-plugin-react": "^7.32.2",
"jest": "^29.5.0",
"jest-expect-message": "^1.1.3",
"supertest": "^6.3.3"
}
}

View File

@@ -1,144 +0,0 @@
/* eslint-disable max-lines */
const Markdown = require('naturalcrit/markdown.js');
test('Renders a mustache span with text only', function() {
const source = '{{ text}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block ">text</span>');
});
test('Renders a mustache span with text only, but with spaces', function() {
const source = '{{ this is a text}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block ">this is a text</span>');
});
test('Renders an empty mustache span', function() {
const source = '{{}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block "></span>');
});
test('Renders a mustache span with just a space', function() {
const source = '{{ }}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block "></span>');
});
test('Renders a mustache span with a few spaces only', function() {
const source = '{{ }}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block "></span>');
});
test('Renders a mustache span with text and class', function() {
const source = '{{my-class text}}';
const rendered = Markdown.render(source);
// FIXME: why do we have those two extra spaces after closing "?
expect(rendered).toBe('<span class="inline-block my-class" >text</span>');
});
test('Renders a mustache span with text and two classes', function() {
const source = '{{my-class,my-class2 text}}';
const rendered = Markdown.render(source);
// FIXME: why do we have those two extra spaces after closing "?
expect(rendered).toBe('<span class="inline-block my-class my-class2" >text</span>');
});
test('Renders a mustache span with text with spaces and class', function() {
const source = '{{my-class this is a text}}';
const rendered = Markdown.render(source);
// FIXME: why do we have those two extra spaces after closing "?
expect(rendered).toBe('<span class="inline-block my-class" >this is a text</span>');
});
test('Renders a mustache span with text and id', function() {
const source = '{{#my-span text}}';
const rendered = Markdown.render(source);
// FIXME: why do we have that one extra space after closing "?
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
});
test('Renders a mustache span with text and two ids', function() {
const source = '{{#my-span,#my-favorite-span text}}';
const rendered = Markdown.render(source);
// FIXME: do we need to report an error here somehow?
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
});
test('Renders a mustache span with text and css property', function() {
const source = '{{color:red text}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block " style="color:red;">text</span>');
});
test('Renders a mustache span with text and two css properties', function() {
const source = '{{color:red,padding:5px text}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block " style="color:red; padding:5px;">text</span>');
});
test('Renders a mustache span with text and css property which contains quotes', function() {
const source = '{{font:"trebuchet ms" text}}';
const rendered = Markdown.render(source);
// FIXME: is it correct to remove quotes surrounding css property value?
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text</span>');
});
test('Renders a mustache span with text and two css properties which contains quotes', function() {
const source = '{{font:"trebuchet ms",padding:"5px 10px" text}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms; padding:5px 10px;">text</span>');
});
test('Renders a mustache span with text with quotes and css property which contains quotes', function() {
const source = '{{font:"trebuchet ms" text "with quotes"}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text “with quotes”</span>');
});
test('Renders a mustache span with text, id, class and a couple of css properties', function() {
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
const rendered = Markdown.render(source);
expect(rendered).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
});
test('Two consecutive injections into Inline', function() {
const source = '{{dog Sample Text}}{cat}{toad}';
const rendered = Markdown.render(source);
// FIXME: Drops original attributes in favor of injection, rather than adding.
// FIXME: Doesn't keep the raw text of second injection.
// FIXME: Renders the extra class attribute (which is dropped by the browser).
expect(rendered).toBe('<p><span class=" cat" class="inline-block cat" >Sample Text</span></p>\n');
});
test('Two consecutive injections into Block', function() {
const source = '{{dog\nSample Text\n}}\n{cat}\n{toad}';
const rendered = Markdown.render(source);
// FIXME: Renders the extra class attribute (which is dropped by the browser).
expect(rendered).toBe('<div class=" cat" class="block cat" ><p>Sample Text</p>\n</div><p>{toad}</p>\n');
});
// TODO: add tests for ID with accordance to CSS spec:
//
// From https://drafts.csswg.org/selectors/#id-selectors:
//
// > An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier.
//
// From: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier:
//
// > In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9]
// > and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_);
// > they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
// > Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item).
// > For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
// > Note that Unicode is code-by-code equivalent to ISO 10646 (see [UNICODE] and [ISO10646]).
// TODO: add tests for class with accordance to CSS spec:
//
// From: https://drafts.csswg.org/selectors/#class-html:
//
// > The class selector is given as a full stop (. U+002E) immediately followed by an identifier.

View File

@@ -0,0 +1,376 @@
/* eslint-disable max-lines */
const dedent = require('dedent-tabs').default;
const Markdown = require('naturalcrit/markdown.js');
// Marked.js adds line returns after closing tags on some default tokens.
// This removes those line returns for comparison sake.
String.prototype.trimReturns = function(){
return this.replace(/\r?\n|\r/g, '');
};
// Adding `.failing()` method to `describe` or `it` will make failing tests "pass" as long as they continue to fail.
// Remove the `.failing()` method once you have fixed the issue.
describe('Inline: When using the Inline syntax {{ }}', ()=>{
it.failing('Renders a mustache span with text only', function() {
const source = '{{ text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
});
it.failing('Renders a mustache span with text only, but with spaces', function() {
const source = '{{ this is a text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">this is a text</span>');
});
it.failing('Renders an empty mustache span', function() {
const source = '{{}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
});
it.failing('Renders a mustache span with just a space', function() {
const source = '{{ }}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
});
it.failing('Renders a mustache span with a few spaces only', function() {
const source = '{{ }}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block"></span>');
});
it.failing('Renders a mustache span with text and class', function() {
const source = '{{my-class text}}';
const rendered = Markdown.render(source);
// FIXME: adds two extra \s before closing `>` in opening tag.
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">text</span>');
});
it.failing('Renders a mustache span with text and two classes', function() {
const source = '{{my-class,my-class2 text}}';
const rendered = Markdown.render(source);
// FIXME: adds two extra \s before closing `>` in opening tag.
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class my-class2">text</span>');
});
it.failing('Renders a mustache span with text with spaces and class', function() {
const source = '{{my-class this is a text}}';
const rendered = Markdown.render(source);
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block my-class">this is a text</span>');
});
it.failing('Renders a mustache span with text and id', function() {
const source = '{{#my-span text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
});
it.failing('Renders a mustache span with text and two ids', function() {
const source = '{{#my-span,#my-favorite-span text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" id="my-span">text</span>');
});
it.failing('Renders a mustache span with text and css property', function() {
const source = '{{color:red text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
});
it.failing('Renders a mustache span with text and two css properties', function() {
const source = '{{color:red,padding:5px text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; padding:5px;">text</span>');
});
it.failing('Renders a mustache span with text and css property which contains quotes', function() {
const source = '{{font-family:"trebuchet ms" text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text</span>');
});
it.failing('Renders a mustache span with text and two css properties which contains quotes', function() {
const source = '{{font-family:"trebuchet ms",padding:"5px 10px" text}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms; padding:5px 10px;">text</span>');
});
it.failing('Renders a mustache span with text with quotes and css property which contains quotes', function() {
const source = '{{font-family:"trebuchet ms" text "with quotes"}}';
const rendered = Markdown.render(source);
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="font-family:trebuchet ms;">text “with quotes”</span>');
});
it('Renders a mustache span with text, id, class and a couple of css properties', function() {
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
});
});
// BLOCK SYNTAX
describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
it.failing('Renders a div with text only', function() {
const source = dedent`{{
text
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"><p>text</p></div>`);
});
it.failing('Renders an empty div', function() {
const source = dedent`{{
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block"></div>`);
});
it('Renders a single paragraph with opening and closing brackets', function() {
const source = dedent`{{
}}`;
const rendered = Markdown.render(source).trimReturns();
// this actually renders in HB as '{{ }}'...
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>{{}}</p>`);
});
it.failing('Renders a div with a single class', function() {
const source = dedent`{{cat
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"></div>`);
});
it.failing('Renders a div with a single class and text', function() {
const source = dedent`{{cat
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with two classes and text', function() {
const source = dedent`{{cat,dog
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat dog"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with a style and text', function() {
const source = dedent`{{color:red
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds two extra \s before closing `>` in opening tag
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" style="color:red;"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with a class, style and text', function() {
const source = dedent`{{cat,color:red
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s after the class attribute
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" style="color:red;"><p>Sample text.</p></div>`);
});
it('Renders a div with an ID, class, style and text (different order)', function() {
const source = dedent`{{color:red,cat,#dog
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block cat" id="dog" style="color:red;"><p>Sample text.</p></div>`);
});
it.failing('Renders a div with a single ID', function() {
const source = dedent`{{#cat,#dog
Sample text.
}}`;
const rendered = Markdown.render(source).trimReturns();
// FIXME: adds extra \s before closing `>` in opening tag, and another after class names
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<div class="block" id="cat"><p>Sample text.</p></div>`);
});
});
// MUSTACHE INJECTION SYNTAX
describe('Injection: When an injection tag follows an element', ()=>{
// FIXME: Most of these fail because injections currently replace attributes, rather than append to. Or just minor extra whitespace issues.
describe('and that element is an inline-block', ()=>{
it.failing('Renders a span "text" with no injection', function() {
const source = '{{ text}}{}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block">text</span>');
});
it.failing('Renders a span "text" with injected Class name', function() {
const source = '{{ text}}{ClassName}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block ClassName">text</span>');
});
it.failing('Renders a span "text" with injected style', function() {
const source = '{{ text}}{color:red}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red;">text</span>');
});
it.failing('Renders a span "text" with two injected styles', function() {
const source = '{{ text}}{color:red,background:blue}';
const rendered = Markdown.render(source);
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<span class="inline-block" style="color:red; background:blue;">text</span>');
});
it.failing('Renders an emphasis element with injected Class name', function() {
const source = '*emphasis*{big}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><em class="big">emphasis</em></p>');
});
it.failing('Renders a code element with injected style', function() {
const source = '`code`{background:gray}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><code style="background:gray;">code</code></p>');
});
it.failing('Renders an image element with injected style', function() {
const source = '![alt text](http://i.imgur.com/hMna6G0.png){position:absolute}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><img src="http://i.imgur.com/hMna6G0.png" alt="homebrew mug" style="position:absolute;"></p>');
});
it.failing('Renders an element modified by only the first of two consecutive injections', function() {
const source = '{{ text}}{color:red}{background:blue}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p><span class="inline-block" style="color:red;">text</span>{background:blue}</p>');
});
});
describe('and that element is a block', ()=>{
it.failing('renders a div "text" with no injection', function() {
const source = '{{\ntext\n}}\n{}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block"><p>text</p></div>');
});
it.failing('renders a div "text" with injected Class name', function() {
const source = '{{\ntext\n}}\n{ClassName}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block ClassName"><p>text</p></div>');
});
it.failing('renders a div "text" with injected style', function() {
const source = '{{\ntext\n}}\n{color:red}';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red;"><p>text</p></div>');
});
it.failing('renders a div "text" with two injected styles', function() {
const source = dedent`{{
text
}}
{color:red,background:blue}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block" style="color:red; background:blue;"><p>text</p></div>');
});
it.failing('renders an h2 header "text" with injected class name', function() {
const source = dedent`## text
{ClassName}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2>');
});
it.failing('renders a table with injected class name', function() {
const source = dedent`| Experience Points | Level |
|:------------------|:-----:|
| 0 | 1 |
| 300 | 2 |
{ClassName}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<table class="ClassName"><thead><tr><th align=left>Experience Points</th><th align=center>Level</th></tr></thead><tbody><tr><td align=left>0</td><td align=center>1</td></tr><tr><td align=left>300</td><td align=center>2</td></tr></tbody></table>`);
});
// it('renders a list with with a style injected into the <ul> tag', function() {
// const source = dedent`- Cursed Ritual of Bad Hair
// - Eliminate Vindictiveness in Gym Teacher
// - Ultimate Rite of the Confetti Angel
// - Dark Chant of the Dentists
// - Divine Spell of Crossdressing
// {color:red}`;
// const rendered = Markdown.render(source).trimReturns();
// expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into <ul>? Currently injects into last <li>
// });
it.failing('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
const source = dedent`## text
{ClassName}
{secondInjection}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 class="ClassName">text</h2><p>{secondInjection}</p>');
});
it.failing('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
const source = dedent`{{
outer text
{{
inner text
}}
{innerDiv}
}}
{outerDiv}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<div class="block outerDiv"><p>outer text</p><div class="block innerDiv"><p>inner text</p></div></div>');
});
});
});
// TODO: add tests for ID with accordance to CSS spec:
//
// From https://drafts.csswg.org/selectors/#id-selectors:
//
// > An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier.
//
// From: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier:
//
// > In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9]
// > and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_);
// > they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
// > Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item).
// > For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
// > Note that Unicode is code-by-code equivalent to ISO 10646 (see [UNICODE] and [ISO10646]).
// TODO: add tests for class with accordance to CSS spec:
//
// From: https://drafts.csswg.org/selectors/#class-html:
//
// > The class selector is given as a full stop (. U+002E) immediately followed by an identifier.