diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c48e7d34..461a0dfa6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,7 +48,7 @@ jobs: - image: cimg/node:16.11.0 working_directory: ~/homebrewery - parallelism: 4 + parallelism: 1 steps: - attach_workspace: @@ -61,15 +61,15 @@ jobs: - run: name: Test - Basic command: npm run test:basic - - run: - name: Test - Coverage - command: npm run test:coverage - 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 + - run: + name: Test - Coverage + command: npm run test:coverage workflows: build_and_test: diff --git a/.eslintrc.js b/.eslintrc.js index bc8b5c8cd..74e7bb660 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,11 +11,11 @@ module.exports = { browser : true, node : true }, - plugins : ['react'], + plugins : ['react', 'jest'], rules : { /** Errors **/ 'camelcase' : ['error', { properties: 'never' }], - 'func-style' : ['error', 'expression', { allowArrowFunctions: true }], + //'func-style' : ['error', 'expression', { allowArrowFunctions: true }], 'no-array-constructor' : 'error', 'no-iterator' : 'error', 'no-nested-ternary' : 'error', @@ -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: 3 }], /** Warnings **/ 'max-lines' : ['warn', { diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 000000000..207dfda62 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,48 @@ +{ + "extends": [ + "stylelint-config-recess-order", + "stylelint-config-recommended"], + "plugins": [ + "stylelint-stylistic", + "./stylelint_plugins/declaration-colon-align.js", + "./stylelint_plugins/declaration-colon-min-space-before", + "./stylelint_plugins/declaration-block-multi-line-min-declarations" + ], + "customSyntax": "postcss-less", + "rules": { + "no-descending-specificity" : null, + "at-rule-no-unknown" : null, + "function-no-unknown" : null, + "font-family-no-missing-generic-family-keyword" : null, + "font-weight-notation" : "named-where-possible", + "font-family-name-quotes" : "always-unless-keyword", + "stylistic/indentation" : "tab", + "no-duplicate-selectors" : true, + "stylistic/color-hex-case" : "upper", + "color-hex-length" : "long", + "stylistic/selector-combinator-space-after" : "always", + "stylistic/selector-combinator-space-before" : "always", + "stylistic/selector-attribute-operator-space-before" : "never", + "stylistic/selector-attribute-operator-space-after" : "never", + "stylistic/selector-attribute-brackets-space-inside" : "never", + "selector-attribute-quotes" : "always", + "selector-pseudo-element-colon-notation" : "double", + "stylistic/selector-pseudo-class-parentheses-space-inside" : "never", + "stylistic/block-opening-brace-space-before" : "always", + "naturalcrit/declaration-colon-min-space-before" : 1, + "stylistic/declaration-block-trailing-semicolon" : "always", + "stylistic/declaration-colon-space-after" : "always", + "stylistic/number-leading-zero" : "always", + "function-url-quotes" : ["always", { "except": ["empty"] }], + "function-url-scheme-disallowed-list" : ["data","http"], + "comment-whitespace-inside" : "always", + "stylistic/string-quotes" : "single", + "stylistic/media-feature-range-operator-space-before" : "always", + "stylistic/media-feature-range-operator-space-after" : "always", + "stylistic/media-feature-parentheses-space-inside" : "never", + "stylistic/media-feature-colon-space-before" : "always", + "stylistic/media-feature-colon-space-after" : "always", + "naturalcrit/declaration-colon-align" : true, + "naturalcrit/declaration-block-multi-line-min-declarations": 1 + } +} diff --git a/Dockerfile b/Dockerfile index 33adea2b8..82b13ac86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:16.11-alpine +FROM node:18-alpine RUN apk --no-cache add git ENV NODE_ENV=docker @@ -10,11 +10,11 @@ WORKDIR /usr/src/app # This improves caching so we don't have to download the dependencies every time the code changes COPY package.json ./ # --ignore-scripts tells yarn not to run postbuild. We run it explicitly later -RUN yarn install --ignore-scripts +RUN npm install --ignore-scripts # Bundle app source and build application COPY . . -RUN yarn build +RUN npm run build EXPOSE 8000 -CMD [ "yarn", "start" ] +CMD [ "npm", "start" ] diff --git a/changelog.md b/changelog.md index 10899a485..626a992d9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,23 @@ ```css +.beta { + color : white; + padding : 4px 6px; + line-height : 1em; + background : grey; + border-radius : 12px; + font-family : monospace; + font-size : 10px; + font-weight : 800; + margin-top : -5px; + margin-bottom : -5px; +} + +.fac { + height: 1em; + line-height: 2em; + margin-bottom: -0.05cm +} + h5 { font-size: .35cm !important; } @@ -61,17 +80,226 @@ pre { ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). +### Thursday 17/08/2023 - v3.9.2 +{{taskList + +##### Calculuschild + +* [x] Fix links to certain old Google Drive files + +Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917) + +##### G-Ambatte + +* [x] Menus now open on click, and internally consistent + +Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782) + +* [x] Add smarter footer snippet + +Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289) + +* [x] Add sanitization in Style editor + +Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437) + +* [x] Rework class table snippets to remove unnecessary randomness + +Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964) + +* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations + +Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954) + +* [x] Add default save location selection to Account Page + +Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943) + +##### 5e-Cleric + +* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended) + +Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920) + +##### Gazook89 + +* [x] Adjustments to improve mobile viewing + +}} + +### Wednesday 28/06/2023 - v3.9.1 +{{taskList + +##### G-Ambatte + +* [x] Better error pages with more useful information + +Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924) +}} + +### Friday 02/06/2023 - v3.9.0 +{{taskList + +##### Calculuschild + +* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive + +Fixes issue [#2408](https://github.com/naturalcrit/homebrewery/issues/2408) + +* [x] Pressing tab now indents with spaces instead of tab character; fixes several issues with Markdown lists + +Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#1556](https://github.com/naturalcrit/homebrewery/issues/1556) + +* [x] Rename `naturalCritLogo.svg` to `naturalCritLogoRed.svg`. Those using the {{beta BETA}} coverPage snippet may need to update that text to make the NaturalCrit logo appear again. + +##### G-Ambatte + +* [x] Fix strange animation of image masks + +Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790) + +##### 5e-Cleric + +* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3! + +* [x] New {{openSans **PHB → {{fac,book-back-cover}} BACK COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!) + +* [x] New {{openSans **TEXT EDITOR → {{fas,fa-bars}} INDEX** }} snippet for V3! + +* [x] Fix highlighting of curly braces inside comments + +Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784) +}} + +### Wednesday 12/04/2023 - v3.8.0 +{{taskList + +##### calculuschild + +* [x] Rename `{{coverPage}}` to `{{frontCover}}`. Those using this {{beta BETA}} feature will need to update that text to make the cover page appear again. + +* [x] Several background fixes to test scripts + +##### Jeddai + +* [X] Add content negotiation to exclude image requests from our API calls + +Fixes issue [#2595](https://github.com/naturalcrit/homebrewery/issues/2595) + +##### G-Ambatte + +* [x] Update server build scripts to fix Admin page + +Fixes issues [#2657](https://github.com/naturalcrit/homebrewery/issues/2657) + +* [x] Fix internal links inside `<\div>` blocks not receiving the `target=_self` attribute + +Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680) + +* [x] See brew details on `/share` pages by clicking the brew title (author, last update, tags, etc.) + +Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679) + +* [x] Add local Windows install script via Chocolatey + +##### 5e-Cleric + +* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table. + +* [x] New {{openSans **IMAGES → {{fac,mask-center}} WATERCOLOR CENTER** }} snippets for V3, which adds a stylish watercolor texture to the center of your images! + +* [x] New {{openSans **PHB → {{fac,book-inside-cover}} INSIDE COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!) + +* [x] Add some missing characters {{font-family:scalySansRemake Ñ ñ ç Ç Ý ý # ^ ¿ ' " ¡ ·}} to the "scalySansRemake" font in V3. + +Fixes issues [#2280](https://github.com/naturalcrit/homebrewery/issues/2280) + +##### Gazook89 + +* [x] Add "Language" selector in {{fa,fa-info-circle}} **Properties** menu. Sets the HTML Lang attribute for your brew to better handle hyphenation or spellcheck. + +Fixes issues [#1343](https://github.com/naturalcrit/homebrewery/issues/1343) + +* [x] Fix a crash when multiple `{injection}` tags appear in sequence + +Fixes issues [#2712](https://github.com/naturalcrit/homebrewery/issues/2712) + +##### MichielDeMey + +* [x] Remove all-caps display on Account button since usernames are case-sensitive. + +Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731) + +}} + +\page + +### Monday 13/03/2023 - v3.7.2 +{{taskList + +##### Calculuschild + +* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy +}} + +### Thursday 09/03/2023 - v3.7.1 +{{taskList + +##### Lucastucious (new contributor!) + +* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making PDF text selectable + +Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569) + +{{note +**NOTE:** If you create your PDF on a computer with an old version of Mac Preview (v10 or older) you may see shadows appear as solid gray. +}} + +##### MichielDeMey + +* [x] Updated the Google Drive icon +* [x] Backend fix to unit tests failing intermittently + +##### Calculuschild + +* [x] Fix PDF pixelation on CoverPage text outlines +}} + +### Tuesday 28/02/2023 - v3.7.0 +{{taskList + +{{note +**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback. +}} + +##### Calculuschild + +* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!) + +* [x] Fix site not displaying on iOS devices + +##### 5e-Cleric + +* [x] New {{openSans **PHB → COVER PAGE** {{fac,book-front-cover}} }} snippet for V3, which adds a stylish coverpage to your brew! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!) + +##### MichielDeMey (new contribuor!) + +* [x] Fix typo in testing scripts +* [x] Fix "mug" image not using HTTPS + +Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687) +}} + ### Saturday 18/02/2023 - v3.6.1 {{taskList ##### G-Ambatte -* [x] Fix users not being removed from Authors list correctly +* [x] Fix users not being removed from Authors list Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674) }} - -### Friday 23/01/2023 - v3.6.0 +### Monday 23/01/2023 - v3.6.0 {{taskList ##### calculuschild @@ -96,6 +324,8 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583) * [x] Fix cloned brews inheriting the parent view count }} +\page + ### Friday 23/12/2022 - v3.5.0 {{taskList @@ -109,8 +339,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583) Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987) }} -\page - ### Saturday 10/12/2022 - v3.4.2 {{taskList diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx new file mode 100644 index 000000000..a6e699dcf --- /dev/null +++ b/client/components/combobox.jsx @@ -0,0 +1,129 @@ +const React = require('react'); +const createClass = require('create-react-class'); +const _ = require('lodash'); +const cx = require('classnames'); +require('./combobox.less'); + +const Combobox = createClass({ + displayName : 'Combobox', + getDefaultProps : function() { + return { + className : '', + trigger : 'hover', + default : '', + placeholder : '', + autoSuggest : { + clearAutoSuggestOnClick : true, + suggestMethod : 'includes', + filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter + }, + }; + }, + getInitialState : function() { + return { + showDropdown : false, + value : '', + options : [...this.props.options], + inputFocused : false + }; + }, + componentDidMount : function() { + if(this.props.trigger == 'click') + document.addEventListener('click', this.handleClickOutside); + this.setState({ + value : this.props.default + }); + }, + componentWillUnmount : function() { + if(this.props.trigger == 'click') + document.removeEventListener('click', this.handleClickOutside); + }, + handleClickOutside : function(e){ + // Close dropdown when clicked outside + if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) { + this.handleDropdown(false); + } + }, + handleDropdown : function(show){ + this.setState({ + showDropdown : show, + inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false + }); + }, + handleInput : function(e){ + e.persist(); + this.setState({ + value : e.target.value, + inputFocused : false + }, ()=>{ + this.props.onEntry(e); + }); + }, + handleSelect : function(e){ + this.setState({ + value : e.currentTarget.getAttribute('data-value') + }, ()=>{this.props.onSelect(this.state.value);}); + ; + }, + renderTextInput : function(){ + return ( +
{this.handleDropdown(true);} : undefined} + onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}> + this.handleInput(e)} + value={this.state.value || ''} + placeholder={this.props.placeholder} + onBlur={(e)=>{ + if(!e.target.checkValidity()){ + this.setState({ + value : this.props.default + }, ()=>this.props.onEntry(e)); + } + }} + /> +
+ ); + }, + renderDropdown : function(dropdownChildren){ + if(!this.state.showDropdown) return null; + if(this.props.autoSuggest && !this.state.inputFocused){ + const suggestMethod = this.props.autoSuggest.suggestMethod; + const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn; + const filteredArrays = filterOn.map((attr)=>{ + const children = dropdownChildren.filter((item)=>{ + if(suggestMethod === 'includes'){ + return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase()); + } else if(suggestMethod === 'startsWith'){ + return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase()); + } + }); + return children; + }); + dropdownChildren = _.uniq(filteredArrays.flat(1)); + } + + return ( +
+ {dropdownChildren} +
+ ); + }, + render : function () { + const dropdownChildren = this.state.options.map((child, i)=>{ + const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) }); + return clone; + }); + return ( +
{this.handleDropdown(false);} : undefined}> + {this.renderTextInput()} + {this.renderDropdown(dropdownChildren)} +
+ ); + } +}); + +module.exports = Combobox; diff --git a/client/components/combobox.less b/client/components/combobox.less new file mode 100644 index 000000000..3810a874e --- /dev/null +++ b/client/components/combobox.less @@ -0,0 +1,50 @@ +.dropdown-container { + position:relative; + input { + width: 100%; + } + .dropdown-options { + position:absolute; + background-color: white; + z-index: 100; + width: 100%; + border: 1px solid gray; + overflow-y: auto; + max-height: 200px; + + &::-webkit-scrollbar { + width: 14px; + } + &::-webkit-scrollbar-track { + background: #ffffff; + } + &::-webkit-scrollbar-thumb { + background-color: #949494; + border-radius: 10px; + border: 3px solid #ffffff; + } + + .item { + position:relative; + font-size: 11px; + font-family: Open Sans; + padding: 5px; + cursor: default; + margin: 0 3px; + //border-bottom: 1px solid darkgray; + &:hover { + filter: brightness(120%); + background-color: rgb(163, 163, 163); + } + .detail { + width:100%; + text-align: left; + color: rgb(124, 124, 124); + font-style:italic; + font-size: 9px; + } + } + + } + +} diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index f8a5fce0a..51921c8ca 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -27,6 +27,7 @@ const BrewRenderer = createClass({ style : '', renderer : 'legacy', theme : '5ePHB', + lang : '', errors : [] }; }, @@ -107,6 +108,12 @@ const BrewRenderer = createClass({ return false; }, + sanitizeScriptTags : function(content) { + return content + .replace(/'; - const rendered = Markdown.render(source); - expect(rendered).toMatch('<script></script>'); -}); - test('Processes the markdown within an HTML block if its just a class wrapper', function() { const source = '
*Bold text*
'; const rendered = Markdown.render(source); expect(rendered).toBe('

Bold text

\n
'); }); + +test('Check markdown is using the custom renderer; specifically that it adds target=_self attribute to internal links in HTML blocks', function() { + const source = '
[Has _self Attribute?](#p1)
'; + const rendered = Markdown.render(source); + expect(rendered).toBe('

Has _self Attribute?

\n
'); +}); diff --git a/tests/markdown/mustache-span.test.js b/tests/markdown/mustache-span.test.js deleted file mode 100644 index 6d249ebcb..000000000 --- a/tests/markdown/mustache-span.test.js +++ /dev/null @@ -1,128 +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('text'); -}); - -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('this is a text'); -}); - -test('Renders an empty mustache span', function() { - const source = '{{}}'; - const rendered = Markdown.render(source); - expect(rendered).toBe(''); -}); - -test('Renders a mustache span with just a space', function() { - const source = '{{ }}'; - const rendered = Markdown.render(source); - expect(rendered).toBe(''); -}); - -test('Renders a mustache span with a few spaces only', function() { - const source = '{{ }}'; - const rendered = Markdown.render(source); - expect(rendered).toBe(''); -}); - -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('text'); -}); - -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('text'); -}); - -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('this is a text'); -}); - -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('text'); -}); - -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('text'); -}); - -test('Renders a mustache span with text and css property', function() { - const source = '{{color:red text}}'; - const rendered = Markdown.render(source); - expect(rendered).toBe('text'); -}); - -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('text'); -}); - -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('text'); -}); - -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('text'); -}); - - -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('text “with quotes”'); -}); - -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('text'); -}); - -// 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. - diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js new file mode 100644 index 000000000..d9e1ce6f9 --- /dev/null +++ b/tests/markdown/mustache-syntax.test.js @@ -0,0 +1,375 @@ +/* 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('text'); + }); + + 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('this is a text'); + }); + + 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(''); + }); + + 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(''); + }); + + 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(''); + }); + + 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('text'); + }); + + 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('text'); + }); + + 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('this is a text'); + }); + + 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('text'); + }); + + 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('text'); + }); + + 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('text'); + }); + + 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('text'); + }); + + 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('text'); + }); + + 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('text'); + }); + + + 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 }).toBe('text “with quotes”'); + }); + + 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 }).toBe('text'); + }); +}); + +// 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 }).toBe(`

text

`); + }); + + 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 }).toBe(`
`); + }); + + 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 }).toBe(`

{{}}

`); + }); + + 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 }).toBe(`
`); + }); + + 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 }).toBe(`

Sample text.

`); + }); + + 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 }).toBe(`

Sample text.

`); + }); + + 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(`

Sample text.

`); + }); + + 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(`

Sample text.

`); + }); + + 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(`

Sample text.

`); + }); + + 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(`

Sample text.

`); + }); +}); + +// 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('text'); + }); + + 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('text'); + }); + + 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('text'); + }); + + 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('text'); + }); + + 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('

emphasis

'); + }); + + 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('

code

'); + }); + + 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('

homebrew mug

'); + }); + + 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('

text{background:blue}

'); + }); + }); + + 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('

text

'); + }); + + 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('

text

'); + }); + + 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('

text

'); + }); + + 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('

text

'); + }); + + 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('

text

'); + }); + + 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(`
Experience PointsLevel
01
3002
`); + }); + + // it('renders a list with with a style injected into the