diff --git a/.github/actions/limit-pull-requests/action.yml b/.github/actions/limit-pull-requests/action.yml
new file mode 100644
index 000000000..413fa0624
--- /dev/null
+++ b/.github/actions/limit-pull-requests/action.yml
@@ -0,0 +1,103 @@
+name: Limit pull requests
+description: >
+ Limit the number of open pull requests to the repository created by a user
+author: ZhongRuoyu (from Homebrew repository)
+branding:
+ icon: alert-triangle
+ color: yellow
+
+inputs:
+ token:
+ description: GitHub token
+ required: false
+ default: ${{ github.token }}
+ except-users:
+ description: The users exempted from the limit, one per line
+ required: false
+ # https://docs.github.com/en/graphql/reference/enums#commentauthorassociation
+ except-author-associations:
+ description: The author associations exempted from the limit, one per line
+ required: false
+ comment-limit:
+ description: >
+ Post the comment when the user's number of open pull requests exceeds this
+ number and `comment` is not empty
+ required: true
+ default: "10"
+ comment:
+ description: The comment to post when the limit is reached
+ required: false
+ close-limit:
+ description: >
+ Close the pull request when the user's number of open pull requests
+ exceeds this number and `close` is set to `true`
+ required: true
+ default: "50"
+ close:
+ description: Whether to close the pull request when the limit is reached
+ required: true
+ default: "false"
+
+runs:
+ using: composite
+ steps:
+ - name: Check the number of pull requests
+ id: count-pull-requests
+ run: |
+ # If the user is exempted, assume they have no pull requests.
+ if grep -Fiqx '${{ github.actor }}' <<<"$EXCEPT_USERS"; then
+ echo "::notice::@${{ github.actor }} is exempted from the limit."
+ echo "count=0" >>"$GITHUB_OUTPUT"
+ exit 0
+ fi
+ if grep -Fiqx '${{ github.event.pull_request.author_association }}' <<<"$EXCEPT_AUTHOR_ASSOCIATIONS"; then
+ echo "::notice::@{{ github.actor }} is a ${{ github.event.pull_request.author_association }} exempted from the limit."
+ echo "count=0" >>"$GITHUB_OUTPUT"
+ exit 0
+ fi
+
+ count="$(
+ gh api \
+ --method GET \
+ --header 'Accept: application/vnd.github+json' \
+ --header 'X-GitHub-Api-Version: 2022-11-28' \
+ --field state=open \
+ --paginate \
+ '/repos/{owner}/{repo}/pulls' |
+ jq \
+ --raw-output \
+ --arg USER '${{ github.actor }}' \
+ 'map(select(.user.login == $USER)) | length'
+ )"
+ echo "::notice::@${{ github.actor }} has $count open pull request(s)."
+ echo "count=$count" >>"$GITHUB_OUTPUT"
+ env:
+ GH_REPO: ${{ github.repository }}
+ GH_TOKEN: ${{ inputs.token }}
+ EXCEPT_USERS: ${{ inputs.except-users }}
+ EXCEPT_AUTHOR_ASSOCIATIONS: ${{ inputs.except-author-associations }}
+ shell: bash
+
+ - name: Comment on pull request
+ if: >
+ fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.comment-limit) &&
+ inputs.comment != ''
+ run: |
+ gh pr comment '${{ github.event.pull_request.number }}' \
+ --body="${COMMENT_BODY}"
+ env:
+ GH_REPO: ${{ github.repository }}
+ GH_TOKEN: ${{ inputs.token }}
+ COMMENT_BODY: ${{ inputs.comment }}
+ shell: bash
+
+ - name: Close pull request
+ if: >
+ fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.close-limit) &&
+ inputs.close == 'true'
+ run: |
+ gh pr close '${{ github.event.pull_request.number }}'
+ env:
+ GH_REPO: ${{ github.repository }}
+ GH_TOKEN: ${{ inputs.token }}
+ shell: bash
diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml
new file mode 100644
index 000000000..e5adb2561
--- /dev/null
+++ b/.github/workflows/pr-check.yml
@@ -0,0 +1,29 @@
+name: PR Check
+on: pull_request_target
+env:
+ GH_REPO: ${{ github.repository }}
+ GH_NO_UPDATE_NOTIFIER: 1
+ GH_PROMPT_DISABLED: 1
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+ statuses: write
+jobs:
+ limit-pull-requests:
+ if: always() && github.repository_owner == 'naturalcrit'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name : Run limit-pull-requests action
+ uses: ./.github/actions/limit-pull-requests
+ with:
+ except-users: |
+ dependabot
+ comment-limit: 3
+ comment: |
+ Hi, thanks for your contribution to the Homebrewery! You already have >=3 open pull requests. Consider completing some of your existing PRs before opening new ones. Thanks!
+ close-limit: 5
+ close: false
diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less
index edc8d503b..0406cad29 100644
--- a/client/homebrew/brewRenderer/brewRenderer.less
+++ b/client/homebrew/brewRenderer/brewRenderer.less
@@ -17,11 +17,24 @@
&::-webkit-scrollbar {
width: 20px;
+ &:horizontal{
+ height: 20px;
+ width:auto;
+ }
+ &-thumb {
+ background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
+ &:horizontal{
+ background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
+ }
+ }
+ &-corner {
+ visibility: hidden;
+ }
}
- &::-webkit-scrollbar-thumb {
- width:20px;
- background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
- }
+
+
+
+
}
.pane { position : relative; }
diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx
index a08a39ea0..8c82f33e7 100644
--- a/client/homebrew/homebrew.jsx
+++ b/client/homebrew/homebrew.jsx
@@ -76,7 +76,7 @@ const Homebrew = createClass({
} />
} />
} />
- } />
+ } />
} />
} />
} />
diff --git a/client/homebrew/homebrew.less b/client/homebrew/homebrew.less
index ed86a8c28..828de796f 100644
--- a/client/homebrew/homebrew.less
+++ b/client/homebrew/homebrew.less
@@ -18,10 +18,19 @@
&::-webkit-scrollbar {
width: 20px;
- }
- &::-webkit-scrollbar-thumb {
- width:20px;
- background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
+ &:horizontal{
+ height: 20px;
+ width:auto;
+ }
+ &-thumb {
+ background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
+ &:horizontal{
+ background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
+ }
+ }
+ &-corner {
+ visibility: hidden;
+ }
}
}
}
diff --git a/client/homebrew/pages/accountPage/accountPage.jsx b/client/homebrew/pages/accountPage/accountPage.jsx
index d08832427..598683504 100644
--- a/client/homebrew/pages/accountPage/accountPage.jsx
+++ b/client/homebrew/pages/accountPage/accountPage.jsx
@@ -1,102 +1,82 @@
-const React = require('react');
-const createClass = require('create-react-class');
-const _ = require('lodash');
-const cx = require('classnames');
+const React = require('react');
const moment = require('moment');
-
const UIPage = require('../basePages/uiPage/uiPage.jsx');
-
-const Nav = require('naturalcrit/nav/nav.jsx');
-const Navbar = require('../../navbar/navbar.jsx');
-
-const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
-const Account = require('../../navbar/account.navitem.jsx');
-const NewBrew = require('../../navbar/newbrew.navitem.jsx');
-const HelpNavItem = require('../../navbar/help.navitem.jsx');
-
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
let SAVEKEY = '';
-const AccountPage = createClass({
- displayName : 'AccountPage',
- getDefaultProps : function() {
- return {
- brew : {},
- uiItems : {}
- };
- },
- getInitialState : function() {
- return {
- uiItems : this.props.uiItems
- };
- },
- componentDidMount : function(){
- if(!this.state.saveLocation && this.props.uiItems.username) {
- SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${this.props.uiItems.username}`;
- let saveLocation = window.localStorage.getItem(SAVEKEY);
- saveLocation = saveLocation ?? (this.state.uiItems.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
- this.makeActive(saveLocation);
+const AccountPage = (props)=>{
+ // destructure props and set state for save location
+ const { accountDetails, brew } = props;
+ const [saveLocation, setSaveLocation] = React.useState('');
+
+ // initialize save location from local storage based on user id
+ React.useEffect(()=>{
+ if(!saveLocation && accountDetails.username) {
+ SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
+ // if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
+ let saveLocation = window.localStorage.getItem(SAVEKEY);
+ saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
+ setActiveSaveLocation(saveLocation);
}
- },
+ }, []);
- makeActive : function(newSelection){
- if(this.state.saveLocation == newSelection) return;
+ const setActiveSaveLocation = (newSelection)=>{
+ if(saveLocation === newSelection) return;
window.localStorage.setItem(SAVEKEY, newSelection);
- this.setState({
- saveLocation : newSelection
- });
- },
+ setSaveLocation(newSelection);
+ };
- renderButton : function(name, key, shouldRender=true){
- if(!shouldRender) return;
- return {this.makeActive(key);}}>{name} ;
- },
+ // todo: should this be a set of radio buttons (well styled) since it's either/or choice?
+ const renderSaveLocationButton = (name, key, shouldRender = true)=>{
+ if(!shouldRender) return null;
+ return (
+ {setActiveSaveLocation(key);}}>
+ {name}
+
+ );
+ };
- renderNavItems : function() {
- return
-
-
-
-
-
-
- ;
- },
+ // render the entirety of the account page content
+ const renderAccountPage = ()=>{
+ return (
+ <>
+
`${key}="${value}"`).join(' ')}` : ''}` +
+ `>${this.parser.parseInline(token.tokens)}`; // parseInline to turn child tokens into HTML
}
};
@@ -149,13 +156,13 @@ const mustacheDivs = {
if(match) {
//Find closing delimiter
let blockCount = 0;
- let tags = '';
+ let tags = {};
let endTags = 0;
let endToken = 0;
let delim;
while (delim = blockRegex.exec(match[0])?.[0].trim()) {
- if(!tags) {
- tags = `${processStyleTags(delim.substring(2))}`;
+ if(_.isEmpty(tags)) {
+ tags = processStyleTags(delim.substring(2));
endTags = delim.length + src.indexOf(delim);
}
if(delim.startsWith('{{')) {
@@ -183,7 +190,14 @@ const mustacheDivs = {
}
},
renderer(token) {
- return `
`${key}="${value}"`).join(' ')}` : ''}` +
+ `>${this.parser.parse(token.tokens)}
`; // parse to turn child tokens into HTML
}
};
@@ -199,23 +213,39 @@ const mustacheInjectInline = {
if(!lastToken || lastToken.type == 'mustacheInjectInline')
return false;
- const tags = `${processStyleTags(match[1])}`;
+ const tags = processStyleTags(match[1]);
lastToken.originalType = lastToken.type;
lastToken.type = 'mustacheInjectInline';
- lastToken.tags = tags;
+ lastToken.injectedTags = tags;
return {
- type : 'text', // Should match "name" above
+ type : 'mustacheInjectInline', // Should match "name" above
raw : match[0], // Text to consume from the source
text : ''
};
}
},
renderer(token) {
+ if(!token.originalType){
+ return;
+ }
token.type = token.originalType;
const text = this.parser.parseInline([token]);
- const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
+ const originalTags = extractHTMLStyleTags(text);
+ const injectedTags = token.injectedTags;
+ const tags = {
+ id : injectedTags.id || originalTags.id || null,
+ classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
+ styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
+ attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
+ };
+ const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
if(openingTag) {
- return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
+ return `${openingTag[1]}` +
+ `${tags.classes ? ` class="${tags.classes}"` : ''}` +
+ `${tags.id ? ` id="${tags.id}"` : ''}` +
+ `${tags.styles ? ` style="${tags.styles}"` : ''}` +
+ `${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
+ `${openingTag[2]}`; // parse to turn child tokens into HTML
}
return text;
}
@@ -235,7 +265,7 @@ const mustacheInjectBlock = {
return false;
lastToken.originalType = 'mustacheInjectBlock';
- lastToken.tags = `${processStyleTags(match[1])}`;
+ lastToken.injectedTags = processStyleTags(match[1]);
return {
type : 'mustacheInjectBlock', // Should match "name" above
raw : match[0], // Text to consume from the source
@@ -249,9 +279,22 @@ const mustacheInjectBlock = {
}
token.type = token.originalType;
const text = this.parser.parse([token]);
- const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
+ const originalTags = extractHTMLStyleTags(text);
+ const injectedTags = token.injectedTags;
+ const tags = {
+ id : injectedTags.id || originalTags.id || null,
+ classes : [originalTags.classes, injectedTags.classes].join(' ').trim() || null,
+ styles : [originalTags.styles, injectedTags.styles].join(' ').trim() || null,
+ attributes : Object.assign(originalTags.attributes ?? {}, injectedTags.attributes ?? {})
+ };
+ const openingTag = /(<[^\s<>]+)[^\n<>]*(>.*)/s.exec(text);
if(openingTag) {
- return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
+ return `${openingTag[1]}` +
+ `${tags.classes ? ` class="${tags.classes}"` : ''}` +
+ `${tags.id ? ` id="${tags.id}"` : ''}` +
+ `${tags.styles ? ` style="${tags.styles}"` : ''}` +
+ `${!_.isEmpty(tags.attributes) ? ` ${Object.entries(tags.attributes).map(([key, value]) => `${key}="${value}"`).join(' ')}` : ''}` +
+ `${openingTag[2]}`; // parse to turn child tokens into HTML
}
return text;
}
@@ -294,10 +337,10 @@ const superSubScripts = {
}
};
-const definitionListsInline = {
- name : 'definitionListsInline',
+const definitionListsSingleLine = {
+ name : 'definitionListsSingleLine',
level : 'block',
- start(src) { return src.match(/^[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
+ start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
let match;
@@ -312,7 +355,7 @@ const definitionListsInline = {
}
if(definitions.length) {
return {
- type : 'definitionListsInline',
+ type : 'definitionListsSingleLine',
raw : src.slice(0, endIndex),
definitions
};
@@ -326,10 +369,10 @@ const definitionListsInline = {
}
};
-const definitionListsMultiline = {
- name : 'definitionListsMultiline',
+const definitionListsMultiLine = {
+ name : 'definitionListsMultiLine',
level : 'block',
- start(src) { return src.match(/^[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match
+ start(src) { return src.match(/\n[^\n]*\n::/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) {
const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y;
let match;
@@ -353,7 +396,7 @@ const definitionListsMultiline = {
}
if(definitions.length) {
return {
- type : 'definitionListsMultiline',
+ type : 'definitionListsMultiLine',
raw : src.slice(0, endIndex),
definitions
};
@@ -617,7 +660,7 @@ function MarkedVariables() {
//^=====--------------------< Variable Handling >-------------------=====^//
Marked.use(MarkedVariables());
-Marked.use({ extensions: [definitionListsMultiline, definitionListsInline, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
+Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
@@ -687,15 +730,45 @@ const processStyleTags = (string)=>{
//TODO: can we simplify to just split on commas?
const tags = string.match(/(?:[^, ":=]+|[:=](?:"[^"]*"|))+/g);
- const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0];
- const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('=')));
- const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'));
- const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()) : [];
+ const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0] || null;
+ const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))).join(' ') || null;
+ const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"'))
+ ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
+ .reduce((obj, attr) => {
+ let [key, value] = attr.split("=");
+ value = value.replace(/"/g, '');
+ obj[key] = value;
+ return obj;
+ }, {}) || null;
+ const styles = tags?.length ? tags.map((tag)=>tag.replace(/:"?([^"]*)"?/g, ':$1;').trim()).join(' ') : null;
- return `${classes?.length ? ` ${classes.join(' ')}` : ''}"` +
- `${id ? ` id="${id}"` : ''}` +
- `${styles?.length ? ` style="${styles.join(' ')}"` : ''}` +
- `${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
+ return {
+ id : id,
+ classes : classes,
+ styles : styles,
+ attributes : _.isEmpty(attributes) ? null : attributes
+ };
+};
+
+const extractHTMLStyleTags = (htmlString)=> {
+ const id = htmlString.match(/id="([^"]*)"/)?.[1] || null;
+ const classes = htmlString.match(/class="([^"]*)"/)?.[1] || null;
+ const styles = htmlString.match(/style="([^"]*)"/)?.[1] || null;
+ const attributes = htmlString.match(/[a-zA-Z]+="[^"]*"/g)
+ ?.filter(attr => !attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="'))
+ .reduce((obj, attr) => {
+ let [key, value] = attr.split("=");
+ value = value.replace(/"/g, '');
+ obj[key] = value;
+ return obj;
+ }, {}) || null;
+
+ return {
+ id : id,
+ classes : classes,
+ styles : styles,
+ attributes : _.isEmpty(attributes) ? null : attributes
+ };
};
const globalVarsList = {};
diff --git a/tests/markdown/mustache-syntax.test.js b/tests/markdown/mustache-syntax.test.js
index 835bcc575..8ca12519e 100644
--- a/tests/markdown/mustache-syntax.test.js
+++ b/tests/markdown/mustache-syntax.test.js
@@ -243,61 +243,91 @@ describe(`Block: When using the Block syntax {{tags\\ntext\\n}}`, ()=>{
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() {
+ it('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() {
+ it('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 attribute', function() {
+ it('Renders a span "text" with injected attribute', function() {
const source = '{{ text}}{a="b and c"}';
const rendered = Markdown.render(source);
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
});
- it.failing('Renders a span "text" with injected style', function() {
+ it('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 injected style using a string variable', function() {
+ it('Renders a span "text" with injected style using a string variable', function() {
const source = `{{ text}}{--stringVariable:"'string'"}`;
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() {
+ it('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() {
+ it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
+ const source = '{{#oldId text}}{#newId}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
+ const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new"}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
+ const source = '{{attrA="old",attrB="old" text}}{attrA="new",attrC="new",class="new",style="new",id="new"}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
+ });
+
+ it('Renders a span "text" with its own styles, appended with injected styles', function() {
+ const source = '{{color:blue,height:10px text}}{width:10px,color:red}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
+ });
+
+ it('Renders a span "text" with its own classes, appended with injected classes', function() {
+ const source = '{{classA,classB text}}{classA,classC}';
+ const rendered = Markdown.render(source);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
+ });
+
+ it('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() {
+ it('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() {
+ it('Renders an image element with injected style', function() {
const source = '{position:absolute}';
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
});
- it.failing('Renders an element modified by only the first of two consecutive injections', function() {
+ it('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}
');
@@ -306,55 +336,100 @@ describe('Injection: When an injection tag follows an element', ()=>{
it('Renders an image with added attributes', function() {
const source = ` {position:absolute,bottom:20px,left:130px,width:220px,a="b and c",d=e}`;
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
});
describe('and that element is a block', ()=>{
- it.failing('renders a div "text" with no injection', function() {
+ it('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('
');
});
- it.failing('renders a div "text" with injected Class name', function() {
+ it('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('
');
});
- it.failing('renders a div "text" with injected style', function() {
+ it('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('
');
});
- it.failing('renders a div "text" with two injected styles', function() {
+ it('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(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
- it.failing('renders a div "text" with injected variable string', function() {
+ it('renders a div "text" with injected variable string', function() {
const source = dedent`{{
- text
- }}
- {--stringVariable:"'string'"}`;
+ text
+ }}
+ {--stringVariable:"'string'"}`;
const rendered = Markdown.render(source).trimReturns();
- expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
`);
});
- it.failing('renders an h2 header "text" with injected class name', function() {
+ it('Renders a span "text" with its own ID, overwritten with an injected ID', function() {
+ const source = dedent`{{#oldId
+ text
+ }}
+ {#newId}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, plus a new one', function() {
+ const source = dedent`{{attrA="old",attrB="old"
+ text
+ }}
+ {attrA="new",attrC="new"}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own attributes, overwritten with an injected attribute, ignoring "class", "style", and "id"', function() {
+ const source = dedent`{{attrA="old",attrB="old"
+ text
+ }}
+ {attrA="new",attrC="new",class="new",style="new",id="new"}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own styles, appended with injected styles', function() {
+ const source = dedent`{{color:blue,height:10px
+ text
+ }}
+ {width:10px,color:red}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('Renders a span "text" with its own classes, appended with injected classes', function() {
+ const source = dedent`{{classA,classB
+ text
+ }}
+ {classA,classC}`;
+ const rendered = Markdown.render(source).trimReturns();
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
');
+ });
+
+ it('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 ');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('
text ');
});
- it.failing('renders a table with injected class name', function() {
+ it('renders a table with injected class name', function() {
const source = dedent`| Experience Points | Level |
|:------------------|:-----:|
| 0 | 1 |
@@ -376,15 +451,15 @@ describe('Injection: When an injection tag follows an element', ()=>{
// expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`...`); // FIXME: expect this to be injected into
? Currently injects into last
// });
- it.failing('renders an h2 header "text" with injected class name, and "secondInjection" as regular text on the next line.', function() {
+ it('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('text {secondInjection}
');
+ expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('text {secondInjection}
');
});
- it.failing('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
+ it('renders a div nested into another div, the inner with class=innerDiv and the other class=outerDiv', function() {
const source = dedent`{{
outer text
{{
diff --git a/tests/markdown/variables.test.js b/tests/markdown/variables.test.js
index c909dafec..e6018e19f 100644
--- a/tests/markdown/variables.test.js
+++ b/tests/markdown/variables.test.js
@@ -329,7 +329,7 @@ describe('Normal Links and Images', ()=>{
const source = `{width:100px}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
-
`.trimReturns());
+
`.trimReturns());
});
it('Renders normal links', function() {
diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less
index 37327fb19..57a39e507 100644
--- a/themes/V3/5ePHB/style.less
+++ b/themes/V3/5ePHB/style.less
@@ -1,6 +1,6 @@
@import (less) './themes/assets/assets.less';
@import (less) './themes/fonts/icon fonts/font-icons.less';
-@import (less) './themes/fonts/icon fonts/dicefont.less';
+@import (less) './themes/fonts/icon fonts/diceFont.less';
:root {
//Colors
@@ -543,10 +543,8 @@
color : white;
text-shadow : unset;
text-transform : uppercase;
- filter : drop-shadow(0 0 1.5px black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
+ -webkit-text-stroke: 0.2cm black;
+ paint-order:stroke;
}
h2 {
font-family : 'NodestoCapsCondensed';
@@ -554,10 +552,8 @@
font-weight : normal;
color : white;
letter-spacing : 0.1cm;
- filter : drop-shadow(0 0 1px black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
+ -webkit-text-stroke: 0.14cm black;
+ paint-order:stroke;
}
hr {
position : relative;
@@ -603,10 +599,8 @@
font-size : 0.496cm;
color : white;
text-align : center;
- filter : drop-shadow(0 0 0.7px black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
- drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
+ -webkit-text-stroke: 0.1cm black;
+ paint-order:stroke;
}
.logo {
position : absolute;
diff --git a/themes/V3/Blank/style.less b/themes/V3/Blank/style.less
index 1d8ca6ee4..ec8905630 100644
--- a/themes/V3/Blank/style.less
+++ b/themes/V3/Blank/style.less
@@ -1,6 +1,6 @@
@import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less';
-@import (less) './themes/fonts/icon fonts/dicefont.less';
+@import (less) './themes/fonts/icon fonts/diceFont.less';
:root {
//Colors
diff --git a/themes/fonts/icon fonts/dicefont.less b/themes/fonts/icon fonts/diceFont.less
similarity index 98%
rename from themes/fonts/icon fonts/dicefont.less
rename to themes/fonts/icon fonts/diceFont.less
index 78a88f03a..069f6f769 100644
--- a/themes/fonts/icon fonts/dicefont.less
+++ b/themes/fonts/icon fonts/diceFont.less
@@ -1,9 +1,9 @@
-/* Icon Font: dicefont */
+/* Icon Font: diceFont */
@font-face {
font-family : 'DiceFont';
font-style : normal;
font-weight : normal;
- src : url('../../../fonts/icon fonts/dicefont.woff2');
+ src : url('../../../fonts/icon fonts/diceFont.woff2');
}
.df {
diff --git a/themes/fonts/icon fonts/dicefont.woff2 b/themes/fonts/icon fonts/diceFont.woff2
similarity index 100%
rename from themes/fonts/icon fonts/dicefont.woff2
rename to themes/fonts/icon fonts/diceFont.woff2
diff --git a/themes/fonts/icon fonts/dicefont_license.md b/themes/fonts/icon fonts/diceFont_license.md
similarity index 100%
rename from themes/fonts/icon fonts/dicefont_license.md
rename to themes/fonts/icon fonts/diceFont_license.md