0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-25 11:53:02 +00:00

Compare commits

..

43 Commits

Author SHA1 Message Date
G.Ambatte
eeec24ae78 Change fetch to use request-middleware instead 2024-02-13 09:14:31 +13:00
G.Ambatte
1d778e3249 Update API route 2024-02-13 09:13:47 +13:00
G.Ambatte
3bb44d8a17 Lint clean up 2024-02-13 09:06:33 +13:00
Víctor Losada Hernández
71c52b4587 fix html response 2024-02-12 08:44:18 +01:00
Víctor Losada Hernández
fe449abb47 trying to figure out pagination 2024-02-10 16:41:39 +01:00
Víctor Losada Hernández
46d1f89b77 minor fixes 2024-01-29 00:12:14 +01:00
Víctor Losada Hernández
bf1f2054de minor fixes 2024-01-28 16:14:54 +01:00
Víctor Losada Hernández
399caaaeff typo 2024-01-28 16:11:32 +01:00
Víctor Losada Hernández
27b4176e23 initial screen 2024-01-28 16:08:44 +01:00
Víctor Losada Hernández
a8bc6b4e1d h2 to h3 2024-01-28 16:04:21 +01:00
Víctor Losada Hernández
3ca8f72762 brewCount 2024-01-28 16:02:19 +01:00
Víctor Losada Hernández
8aec5dbba6 error 500 catch and show 2024-01-28 15:57:51 +01:00
Víctor Losada Hernández
cccebd8494 title and no brews UI 2024-01-28 11:18:37 +01:00
Víctor Losada Hernández
5fbbd92ea7 limit to 1000 2024-01-28 01:12:46 +01:00
Víctor Losada Hernández
81f26e0892 exclude last fix 2024-01-28 00:55:22 +01:00
Víctor Losada Hernández
9366284e1d quick fix 2024-01-28 00:26:35 +01:00
Víctor Losada Hernández
9aa5eea8c9 exclude fields and add a time limit 2024-01-27 23:31:56 +01:00
Víctor Losada Hernández
20f61bff07 limit to 2000 2024-01-27 21:46:28 +01:00
Víctor Losada Hernández
625819da91 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into experimental-development 2024-01-27 19:14:48 +01:00
Víctor Losada Hernández
2c691d84f2 author fix 3 2024-01-27 19:14:38 +01:00
Víctor Losada Hernández
4630d2640b author another fix 2024-01-27 17:29:16 +01:00
Víctor Losada Hernández
043f24d5ca invited authors is not a thing 2024-01-27 16:56:38 +01:00
Víctor Losada Hernández
87e18c0521 no authors fix 2024-01-27 16:56:28 +01:00
Víctor Losada Hernández
7e30f860b2 typo fix 2024-01-27 16:26:06 +01:00
Víctor Losada Hernández
0ac0ffe53d limit to 3000 2024-01-27 16:25:06 +01:00
Víctor Losada Hernández
ae4e1b55e6 minor changes 2024-01-27 16:24:45 +01:00
Víctor Losada Hernández
756ced088c Merge branch 'experimental-development' of https://github.com/5e-Cleric/homebrewery into experimental-development 2024-01-27 14:31:34 +01:00
Víctor Losada Hernández
8ce6b22be7 reject modernity, embrace tradition 2024-01-27 14:31:32 +01:00
Trevor Buckner
6184d64f89 Merge branch 'master' into pr/3263 2024-01-25 16:43:21 -05:00
Víctor Losada Hernández
c00c2626b4 Merge branch 'master' into experimental-development 2024-01-24 22:56:30 +01:00
Víctor Losada Hernández
89fddd0210 limit search and adapt ui 2024-01-24 21:15:26 +01:00
Víctor Losada Hernández
0c167d803c limit the search 2024-01-24 21:15:00 +01:00
Víctor Losada Hernández
c50042c1e7 i love grid template area 2024-01-23 22:55:36 +01:00
Víctor Losada Hernández
0dc1b46466 stying updates, agnostic theme 2024-01-23 19:08:57 +01:00
Víctor Losada Hernández
4ed9fc7d0e fix url params 2024-01-23 18:35:09 +01:00
Víctor Losada Hernández
162929bdca trying to catch url with query 2024-01-23 14:02:27 +01:00
Víctor Losada Hernández
54a2f6940c from admin to archive api 2024-01-23 10:40:53 +01:00
Víctor Losada Hernández
0dff59d793 linting and space issues 2024-01-23 08:07:51 +01:00
Víctor Losada Hernández
7951c4a03a basic ui and small changes 2024-01-23 00:20:18 +01:00
Víctor Losada Hernández
66fd56fccb basic functionality 2024-01-22 23:52:42 +01:00
Víctor Losada Hernández
da699e999f basic looks 2024-01-22 23:20:36 +01:00
Víctor Losada Hernández
c6a5f50c76 admin page fix 2024-01-22 17:22:54 +01:00
Víctor Losada Hernández
74c7395ab9 admin look by title 2024-01-22 16:59:45 +01:00
58 changed files with 2018 additions and 3472 deletions

View File

@@ -64,12 +64,6 @@ jobs:
- run:
name: Test - Mustache Spans
command: npm run test:mustache-syntax
- run:
name: Test - Definition Lists
command: npm run test:definition-lists
- run:
name: Test - Variables
command: npm run test:variables
- run:
name: Test - Routes
command: npm run test:route

View File

@@ -1,103 +0,0 @@
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

View File

@@ -1,25 +0,0 @@
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 == 'Homebrew'
runs-on: ubuntu-latest
steps:
- uses: Homebrew/actions/limit-pull-requests@master
with:
except-users: |
dependabot
comment-limit: 1
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

View File

@@ -75,253 +75,11 @@ pre {
.page {
padding-bottom: 1.5cm;
}
.varSyntaxTable th:first-of-type {
width:6cm;
}
```
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Monday 18/3/2024 - v3.12.0
{{taskList
##### 5e-Cleric
* [x] Fix language-specific hyphenation on print page
Fixes issue [#3294](https://github.com/naturalcrit/homebrewery/issues/3294)
* [x] Upgrade Font-Awesome to v6.51
* [x] Allow downloaded files to be uploaded via {{openSans **NEW {{fa,fa-plus-square}} → FROM UPLOAD {{fa,fa-upload}}**}}
##### G-Ambatte
* [x] Fix an edge case crash with empty documents
Fixes issue [#3315](https://github.com/naturalcrit/homebrewery/issues/3315)
* [x] Brews on the user page can be searched by tag; clicking a tag adds it to the filter
Fixes issue [#3164](https://github.com/naturalcrit/homebrewery/issues/3164)
* [x] Add *DiceFont* icons {{df,d20-20}} `{{df,icon-name}}`
##### abquintic
* [x] Fix ^super^ and ^^sub^^ highlighting in the text editor
* [x] Add new syntax for multiline Definition Lists:
```
Term
::Definition 1
::Definition 2
with more text
```
produces:
Term
::Definition 1
::Definition 2
with more text
Fixes issue [#2340](https://github.com/naturalcrit/homebrewery/issues/2340)
##### RKuerten :
* [x] Fix monster stat block backgrounds on print page
Fixes issue [#3275](https://github.com/naturalcrit/homebrewery/issues/3275)
* [x] Added new text editor theme: "Darkvision".
##### calculuschild, G-Ambatte, 5e-Cleric
* [x] Codebase and UI cleanup
}}
\page
### Friday 21/2/2024 - v3.11.0
{{taskList
##### Gazook89
* [x] Brew view count no longer increases when viewed by owner
Fixes issue [#3037](https://github.com/naturalcrit/homebrewery/issues/3037)
* [x] Small tweak to PHB H3 sizing
Fixes issue [#2989](https://github.com/naturalcrit/homebrewery/issues/2989)
* [x] Add **Fold/Unfold All** {{fas,fa-compress-alt}} / {{fas,fa-expand-alt}} buttons to editor bar
Fixes issue [#2965](https://github.com/naturalcrit/homebrewery/issues/2965)
##### G-Ambatte
* [x] Share link added to Editor Access error page
Fixes issue [#3086](https://github.com/naturalcrit/homebrewery/issues/3086)
* [x] Add Darkbrewery theme to Editor theme selector {{fas,fa-palette}}
Fixes issue [#3034](https://github.com/naturalcrit/homebrewery/issues/3034)
* [x] Fix Firefox prints with alternating blank pages
Fixes issue [#3115](https://github.com/naturalcrit/homebrewery/issues/3115)
* [x] Admin page working again
Fixes issue [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
##### 5e-Cleric
* [x] Fix indenting issue with Monster Blocks and italics in Class Feature
Fixes issues [#527](https://github.com/naturalcrit/homebrewery/issues/527),
[#3247](https://github.com/naturalcrit/homebrewery/issues/3247)
* [x] Allow CSS vars in curly syntax to be formatted as strings using single quotes
`{{--customVar:"'a string'"}}`
Fixes issue [#3066](https://github.com/naturalcrit/homebrewery/issues/3066)
* [x] Add *Elderberry Inn* icons {{ei,action}} `{{ei,icon-name}}`
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
* [x] New {{openSans **{{fas,fa-keyboard}} FONTS** }} snippets!
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
* [x] New page now opens in a new tab
##### abquintic (new contributor!)
* [x] Add ^super^ `^abc^` and ^^sub^^ `^^abc^^` syntax.
Fixes issue [#2171](https://github.com/naturalcrit/homebrewery/issues/2171)
* [x] Add HTML tag assignment to curly syntax `{{tag=value}}`
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
* [x] {{openSans **Brew → Clone to New**}} now clones tags
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
##### calculuschild
* [x] Better error messages for "Out of Google Drive Storage" and "Not logged in to edit"
Fixes issues [2510](https://github.com/naturalcrit/homebrewery/issues/2510),
[2975](https://github.com/naturalcrit/homebrewery/issues/2975)
* [x] Brew Variables
}}
\
{{wide
### Brew Variable Syntax
You may already be familiar with `[link](url)` and `![image](url)` synax. We have expanded this to include a third `$[variable](text)` syntax. All three of these syntaxes now share a common set of features:
{{varSyntaxTable
| syntax | description |
|:-------|-------------|
| `[var]:content` | Assigns a variable (must start on a line by itself, and ends at the next blank line) |
| `[var](content)` | Assigns a variable and outputs it (can be inline) |
| `[var]` | Outputs the variable contents as a link, if formatted as a valid link |
| `![var]` | Outputs as an image, if formatted as a valid image |
| `$[var]` | Outputs as Markdown |
| `$[var1 + var2 - 2 * var3]` | Performs math operations and outputs result if all variables are valid numbers |
}}
}}
{{wide,margin-top:0,margin-bottom:0
### Examples
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
[first]: Bob
[last]: Jones
My name is $[first] $[last].
```
\column
[first]: Bob
[last]: Jones
My name is $[first] $[last].
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
[myTable]:
| h1 | h2 |
|----|----|
| c1 | c2 |
Here is my table:
$[myTable]
```
\column
[myTable]:
| h1 | h2 |
|----|----|
| c1 | c2 |
Here is my table:
$[myTable]
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
There are $[TableNum] tables total.
#### Table $[TableNum](1): Horses
#### Table $[TableNum]($[TableNum + 1]): Cows
```
\column
There are $[TableNum] tables in this document. *(note: final value of `$[TableNum]` gets hoisted up if available)*
#### Table $[TableNum](1): Horses
#### Table $[TableNum]($[TableNum + 1]): Cows
}}
\page
### Friday 13/10/2023 - v3.10.0
{{taskList
@@ -1577,7 +1335,7 @@ myStyle {color: black}
### Sunday, 29/05/2016 - v2.1.0
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
- The onboarding flow has also been confusing a few users (Homepage new save edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
- The onboarding flow has also been confusing a few users (Homepage -> new -> save -> edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
- Paragraphs now indent properly after lists (thanks u/slitjen!)

View File

@@ -34,6 +34,8 @@ const Stats = createClass({
<dl>
<dt>Total Brew Count</dt>
<dd>{this.state.stats.totalBrews}</dd>
<dt>Total Brews Published Count</dt>
<dd>{this.state.stats.totalPublishedBrews || 'no published brews'}</dd>
</dl>
{this.state.fetching

View File

@@ -20,9 +20,9 @@ const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
<link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`;
@@ -89,24 +89,22 @@ const BrewRenderer = (props)=>{
}));
};
const isInView = (index)=>{
if(!state.isMounted)
return false;
if(index == props.currentEditorPage) //Already rendered before this step
return false;
const shouldRender = (index)=>{
if(!state.isMounted) return false;
if(Math.abs(index - state.viewablePageNumber) <= 3)
return true;
if(index + 1 == props.currentEditorPage)
return true;
return false;
};
const sanitizeScriptTags = (content)=>{
return content
?.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;')
|| '';
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const renderPageInfo = ()=>{
@@ -140,7 +138,7 @@ const BrewRenderer = (props)=>{
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
} else {
cleanPageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
const html = Markdown.render(cleanPageText, index);
const html = Markdown.render(cleanPageText);
return <BrewPage className='page' index={index} key={index} contents={html} />;
}
};
@@ -152,11 +150,8 @@ const BrewRenderer = (props)=>{
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
renderedPages.length = 0;
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
_.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
if((shouldRender(index) || !renderedPages[index]) && typeof window !== 'undefined'){
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
}
});
@@ -211,11 +206,11 @@ const BrewRenderer = (props)=>{
<RenderWarnings />
<NotificationPopup />
</div>
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
}
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted

View File

@@ -14,28 +14,6 @@
box-shadow : 1px 4px 14px #000000;
}
}
&::-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;
}
}
}
.pane { position : relative; }
.pageInfo {

View File

@@ -25,10 +25,13 @@ const NotificationPopup = createClass({
return (
<>
<li key='psa'>
<em>Don't store IMAGES in Google Drive</em><br />
Google Drive is not an image service, and will block images from being used
in brews if they get more views than expected. Google has confirmed they won't fix
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
<em>Broken default logo on <b>CoverPage</b> </em> <br />
If you have used the Cover Page snippet and notice the Naturalcrit
logo is showing as a broken image, this is due to some small tweaks
of this BETA feature. To fix the logo in your cover page, rename
the image link <b>"/assets/naturalCritLogoRed.svg"</b>. Remember
that any snippet marked "BETA" may have a similar change in the
future as we encounter any bugs or reworks.
</li>
<li key='googleDriveFolder'>

View File

@@ -81,7 +81,7 @@ const Editor = createClass({
updateEditorSize : function() {
if(this.refs.codeEditor) {
let paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT;
paneHeight -= SNIPPETBAR_HEIGHT + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
}
},
@@ -151,37 +151,30 @@ const Editor = createClass({
// definition lists
if(line.includes('::')){
if(/^:*$/.test(line) == true){ return };
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
let match;
while ((match = regex.exec(line)) != null){
codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
const ddIndex = match.indices[2][0];
let colons = /::/g;
let colonMatches = colons.exec(match[2]);
if(colonMatches !== null){
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
}
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[0]) }, { line: lineNumber, ch: line.indexOf(match[0]) + match[0].length }, { className: 'define' });
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'term' });
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[2]) }, { line: lineNumber, ch: line.indexOf(match[2]) + match[2].length }, { className: 'definition' });
}
}
// Subscript & Superscript
if(line.includes('^')) {
let startIndex = line.indexOf('^');
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
while (startIndex >= 0) {
superRegex.lastIndex = subRegex.lastIndex = startIndex;
let isSuper = false;
let match = subRegex.exec(line) || superRegex.exec(line);
if (match) {
isSuper = !subRegex.lastIndex;
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
}
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
// Superscript
if(line.includes('\^')) {
const regex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/g;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 1 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 1 }, { className: 'superscript' });
}
}
// Subscript
if(line.includes('^^')) {
const regex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/g;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) - 2 }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length + 2 }, { className: 'subscript' });
}
}

View File

@@ -55,16 +55,6 @@
vertical-align : sub;
font-size : 0.9em;
}
.dl-highlight {
&.dl-colon-highlight {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
&.dt-highlight { color : rgb(96, 117, 143); }
&.dd-highlight { color : rgb(97, 57, 178); }
}
}
.brewJump {

View File

@@ -130,8 +130,6 @@
height : 1.2em;
margin-right : 8px;
font-size : 1.2em;
min-width: 25px;
text-align: center;
& ~ i {
margin-right : 0;
margin-left : 5px;
@@ -140,7 +138,7 @@
&.font {
height : auto;
&::before {
font-size : 1em;
font-size : 1.4em;
content : 'ABC';
}

View File

@@ -11,6 +11,7 @@ const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx');
const ArchivePage = require('./pages/archivePage/archivePage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx');
const WithRoute = (props)=>{
@@ -74,9 +75,10 @@ const Homebrew = createClass({
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
<Route path='/print' element={<WithRoute el={PrintPage} />} />
<Route path='/archive' element={<WithRoute el={ArchivePage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />

View File

@@ -15,23 +15,6 @@
}
&.listPage .content {
overflow-y : scroll;
&::-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;
}
}
}
}
}

View File

@@ -1,351 +1,272 @@
@import 'naturalcrit/styles/colors.less';
@import "naturalcrit/styles/colors.less";
@navbarHeight : 28px;
@keyframes pinkColoring {
0% { color : pink; }
50% { color : pink; }
75% { color : red; }
100% { color : pink; }
0% {color : pink;}
50% {color : pink;}
75% {color : red;}
100% {color : pink;}
}
@keyframes glideDropDown {
0% {
background-color : #333333;
opacity : 0;
transform : translate(0px, -100%);
}
100% {
background-color : #333333;
opacity : 1;
transform : translate(0px, 0px);
}
}
.homebrew nav {
background-color : #333333;
.navContent {
position : relative;
z-index : 2;
display : flex;
justify-content : space-between;
}
.navSection {
display : flex;
align-items : center;
&:last-child .navItem { border-left : 1px solid #666666; }
}
// "NaturalCrit" logo
.navLogo {
display : block;
margin-top : 0px;
margin-right : 8px;
margin-left : 8px;
color : white;
text-decoration : none;
.homebrewLogo {
.animate(color);
font-family : CodeBold;
font-size : 12px;
color : white;
div {
margin-top : 2px;
margin-bottom : -2px;
}
&:hover {
.name { color : @orange; }
svg { fill : @orange; }
}
svg {
height : 13px;
margin-right : 0.2em;
cursor : pointer;
fill : white;
}
span.name {
font-family : 'CodeLight';
font-size : 15px;
span.crit { font-family : 'CodeBold'; }
small {
font-family : 'Open Sans';
font-size : 0.3em;
font-weight : 800;
text-transform : uppercase;
}
color : @blue;
}
}
.navItem {
#backgroundColorsHover;
.animate(background-color);
padding : 8px 12px;
font-size : 10px;
font-weight : 800;
line-height : 13px;
color : white;
text-decoration : none;
text-transform : uppercase;
cursor : pointer;
background-color : #333333;
i {
float : right;
margin-left : 5px;
font-size : 13px;
}
&.patreon {
border-right : 1px solid #666666;
border-left : 1px solid #666666;
&:hover i { color : red; }
i {
color : pink;
.animate(color);
animation-name : pinkColoring;
animation-duration : 2s;
}
}
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
padding : 2px 12px;
input {
width : 250px;
padding : 2px;
margin : 0;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
background-color : transparent;
border : 1px solid @blue;
outline : none;
}
.charCount {
display : inline-block;
margin-left : 8px;
color : #666666;
text-align : right;
vertical-align : bottom;
&.max { color : @red; }
}
}
&.brewTitle {
flex-grow : 1;
.editTitle.navItem {
padding : 2px 12px;
input {
font-family : "Open Sans", sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
width : 250px;
margin : 0;
padding : 2px;
text-align : center;
text-transform : initial;
color : white;
border : 1px solid @blue;
outline : none;
background-color : transparent;
}
// "The Homebrewery" logo
&.homebrewLogo {
.charCount {
display : inline-block;
margin-left : 8px;
text-align : right;
vertical-align : bottom;
color : #666;
&.max {
color : @red;
}
}
}
.brewTitle.navItem {
font-size : 12px;
font-weight : 800;
height : 100%;
text-align : center;
text-transform : initial;
color : white;
background-color : transparent;
flex-grow : 1;
}
.save-menu {
.dropdown {
z-index : 1000;
}
.navItem i.fa-power-off {
color : red;
&.active {
color : rgb(0, 182, 52);
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
}
}
}
.patreon.navItem {
border-right : 1px solid #666;
border-left : 1px solid #666;
&:hover i {
color : red;
}
i {
.animate(color);
font-family : 'CodeBold';
font-size : 12px;
color : white;
div {
margin-top : 2px;
margin-bottom : -2px;
}
&:hover { color : @blue; }
}
&.metadata {
position : relative;
display : flex;
flex-grow : 1;
align-items : center;
height : 100%;
padding : 0;
i { margin-right : 10px;}
.window {
position : absolute;
bottom : 0;
left : 50%;
z-index : -1;
display : flex;
flex-flow : row wrap;
align-content : baseline;
justify-content : flex-start;
width : 440px;
max-height : ~'calc(100vh - 28px)';
padding : 0 10px 5px;
margin : 0 auto;
background-color : #333333;
border : 3px solid #444444;
border-top : unset;
border-radius : 0 0 5px 5px;
box-shadow : inset 0 7px 9px -7px #111111;
transition : transform 0.4s, opacity 0.4s;
&.active {
opacity : 1;
transform : translateX(-50%) translateY(100%);
}
&.inactive {
opacity : 0;
transform : translateX(-50%) translateY(0%);
}
.row {
display : flex;
flex-flow : row wrap;
width : 100%;
h4 {
box-sizing : border-box;
display : block;
flex-basis : 20%;
flex-grow : 1;
min-width : 76px;
padding : 5px 0;
color : #BBBBBB;
text-align : center;
}
p {
flex-basis : 80%;
flex-grow : 1;
padding : 5px 0;
font-family : 'Open Sans', sans-serif;
font-size : 10px;
font-weight : normal;
text-transform : initial;
.tag {
display : inline-block;
padding : 2px;
margin : 2px 2px;
background-color : #444444;
border : 2px solid grey;
border-radius : 5px;
}
a.userPageLink {
color : white;
text-decoration : none;
&:hover { text-decoration : underline; }
}
}
&:nth-of-type(even) { background-color : #555555; }
}
}
}
&.warning {
position : relative;
color : white;
background-color : @orange;
&:hover > .dropdown { visibility : visible; }
.dropdown {
position : absolute;
top : 28px;
left : 0;
z-index : 10000;
box-sizing : border-box;
display : block;
width : 100%;
padding : 13px 5px;
text-align : center;
visibility : hidden;
background-color : #333333;
}
}
&.account {
min-width : 100px;
&.username { text-transform : none;}
animation-name : pinkColoring;
animation-duration : 2s;
color : pink;
}
}
.navDropdownContainer {
.recent.navDropdownContainer {
position : relative;
.navDropdown {
position: absolute;
top: 28px;
right: 0px;
z-index: 10000;
width: max-content;
min-width:100%;
max-height: calc(100vh - 28px);
overflow: hidden auto;
display: flex;
flex-direction: column;
align-items: flex-end;
.navItem {
position : relative;
display : flex;
justify-content : space-between;
align-items : center;
width : 100%;
border : 1px solid #888888;
border-bottom : 0;
animation-name : glideDropDown;
animation-duration : 0.4s;
}
}
&.recent {
position : relative;
.navDropdown .navItem {
#backgroundColorsHover;
.animate(background-color);
position : relative;
box-sizing : border-box;
display : block;
max-width : 15em;
max-height : ~'calc(100vh - 28px)';
padding : 8px 5px 13px;
overflow : hidden auto;
color : white;
text-decoration : none;
background-color : #333333;
border-top : 1px solid #888888;
scrollbar-color : #666666 #333333;
scrollbar-width : thin;
.clear {
position : absolute;
top : 50%;
right : 0;
display : none;
width : 20px;
height : 100%;
background-color : #333333;
border-radius : 3px;
opacity : 70%;
transform : translateY(-50%);
&:hover { opacity : 100%; }
i {
width : 100%;
height : 100%;
margin : 0;
font-size : 10px;
text-align : center;
}
}
&:hover {
background-color : @blue;
.clear {
display : grid;
place-content : center;
}
}
.title {
display : inline-block;
width : 100%;
overflow : hidden auto;
text-overflow : ellipsis;
white-space : nowrap;
}
.time {
position : absolute;
right : 2px;
bottom : 2px;
font-size : 0.7em;
color : #888888;
}
&.header {
box-sizing : border-box;
display : block;
padding : 5px 0;
color : #BBBBBB;
text-align : center;
background-color : #333333;
border-top : 1px solid #888888;
&:nth-of-type(1) { background-color : darken(@teal, 20%); }
&:nth-of-type(2) { background-color : darken(@purple, 30%); }
}
}
}
}
}
.navDropdown .navItem {
overflow : hidden auto;
max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333;
scrollbar-width : thin;
// this should likely be refactored into .navDropdownContainer
.save-menu {
.dropdown { z-index : 1000; }
.navItem i.fa-power-off {
color : red;
&.active {
color : rgb(0, 182, 52);
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
#backgroundColorsHover;
.animate(background-color);
position : relative;
display : block;
overflow : clip;
box-sizing : border-box;
padding : 8px 5px 13px;
text-decoration : none;
color : white;
border-top : 1px solid #888;
background-color : #333;
.clear {
position : absolute;
top : 50%;
right : 0;
display : none;
width : 20px;
height : 100%;
transform : translateY(-50%);
opacity : 70%;
border-radius : 3px;
background-color : #333;
&:hover {
opacity : 100%;
}
i {
font-size : 10px;
width : 100%;
height : 100%;
margin : 0;
text-align : center;
}
}
&:hover {
background-color : @blue;
.clear {
display : grid;
place-content : center;
}
}
.title {
display : inline-block;
overflow : hidden;
width : 100%;
white-space : nowrap;
text-overflow : ellipsis;
}
.time {
font-size : 0.7em;
position : absolute;
right : 2px;
bottom : 2px;
color : #888;
}
&.header {
display : block;
box-sizing : border-box;
padding : 5px 0;
text-align : center;
color : #BBB;
border-top : 1px solid #888;
background-color : #333;
&:nth-of-type(1) {
background-color : darken(@teal, 20%);
}
&:nth-of-type(2) {
background-color : darken(@purple, 30%);
}
}
}
}
.metadata.navItem {
position : relative;
display : flex;
align-items : center;
height : 100%;
padding : 0;
flex-grow : 1;
i {
margin-right : 10px;
}
.window {
position : absolute;
z-index : -1;
bottom : 0;
left : 50%;
display : flex;
justify-content : flex-start;
width : 440px;
max-height : ~"calc(100vh - 28px)";
margin : 0 auto;
padding : 0 10px 5px;
transition : transform 0.4s, opacity 0.4s;
border : 3px solid #444;
border-top : unset;
border-radius : 0 0 5px 5px;
background-color : #333;
box-shadow : inset 0 7px 9px -7px #111;
flex-flow : row wrap;
align-content : baseline;
&.active {
transform : translateX(-50%) translateY(100%);
opacity : 1;
}
&.inactive {
transform : translateX(-50%) translateY(0%);
opacity : 0;
}
.row {
display : flex;
width : 100%;
flex-flow : row wrap;
h4 {
display : block;
box-sizing : border-box;
min-width : 76px;
padding : 5px 0;
text-align : center;
color : #BBB;
flex-basis : 20%;
flex-grow : 1;
}
p {
font-family : "Open Sans", sans-serif;
font-size : 10px;
font-weight : normal;
padding : 5px 0;
text-transform : initial;
flex-basis : 80%;
flex-grow : 1;
.tag {
display : inline-block;
margin : 2px 2px;
padding : 2px;
border : 2px solid grey;
border-radius : 5px;
background-color : #444;
}
a.userPageLink {
text-decoration : none;
color : white;
&:hover {
text-decoration : underline;
}
}
}
&:nth-of-type(even) {
background-color : #555;
}
}
}
}
.warning.navItem {
position : relative;
color : white;
background-color : @orange;
&:hover > .dropdown {
visibility : visible;
}
.dropdown {
position : absolute;
z-index : 10000;
top : 28px;
left : 0;
display : block;
visibility : hidden;
box-sizing : border-box;
width : 100%;
padding : 13px 5px;
text-align : center;
background-color : #333;
}
}
.account.navItem {
min-width : 100px;
}
.account.username.navItem {
text-transform : none;
}
}

View File

@@ -1,64 +1,12 @@
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const NewBrew = () => {
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const fileContent = e.target.result;
const newBrew = {
text: fileContent,
style: ''
};
if(fileContent.startsWith('```metadata')) {
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style);
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
window.location.href = '/new';
} else {
alert('This file is invalid, please, enter a valid file');
}
};
reader.readAsText(file);
}
};
return (
<Nav.dropdown>
<Nav.item
className='new'
color='purple'
icon='fa-solid fa-plus-square'>
new
</Nav.item>
<Nav.item
className='fromBlank'
href='/new'
newTab={true}
color='purple'
icon='fa-solid fa-file'>
from blank
</Nav.item>
<Nav.item
className='fromFile'
color='purple'
icon='fa-solid fa-upload'
onClick={() => { document.getElementById('uploadTxt').click(); }}>
<input id="uploadTxt" className='newFromLocal' type="file" onChange={handleFileChange} style={{ display: 'none' }} />
from file
</Nav.item>
</Nav.dropdown>
);
module.exports = function(props){
return <Nav.item
href='/new'
newTab={true}
color='purple'
icon='fas fa-plus-square'>
new
</Nav.item>;
};
module.exports = NewBrew;

View File

@@ -1,82 +1,102 @@
const React = require('react');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
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 = (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);
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 setActiveSaveLocation = (newSelection)=>{
if(saveLocation === newSelection) return;
makeActive : function(newSelection){
if(this.state.saveLocation == newSelection) return;
window.localStorage.setItem(SAVEKEY, newSelection);
setSaveLocation(newSelection);
};
this.setState({
saveLocation : newSelection
});
},
// 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 (
<button className={saveLocation === key ? 'active' : ''} onClick={()=>{setActiveSaveLocation(key);}}>
{name}
</button>
);
};
renderButton : function(name, key, shouldRender=true){
if(!shouldRender) return;
return <button className={this.state.saveLocation==key ? 'active' : ''} onClick={()=>{this.makeActive(key);}}>{name}</button>;
},
// render the entirety of the account page content
const renderAccountPage = ()=>{
return (
<>
<div className='dataGroup'>
<h1>Account Information <i className='fas fa-user'></i></h1>
<p><strong>Username: </strong>{accountDetails.username || 'No user currently logged in'}</p>
<p><strong>Last Login: </strong>{moment(accountDetails.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
</div>
<div className='dataGroup'>
<h3>Homebrewery Information <NaturalCritIcon /></h3>
<p><strong>Brews on Homebrewery: </strong>{accountDetails.mongoCount}</p>
</div>
<div className='dataGroup'>
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
<p><strong>Linked to Google: </strong>{accountDetails.googleId ? 'YES' : 'NO'}</p>
{accountDetails.googleId && (
<p>
<strong>Brews on Google Drive: </strong>{accountDetails.googleCount ?? (
<>
Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a>
</>
)}
</p>
)}
</div>
<div className='dataGroup'>
<h4>Default Save Location</h4>
{renderSaveLocationButton('Homebrewery', 'HOMEBREWERY')}
{renderSaveLocationButton('Google Drive', 'GOOGLE-DRIVE', accountDetails.googleId)}
</div>
</>
);
};
renderNavItems : function() {
return <Navbar>
<Nav.section>
<NewBrew />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>;
},
// return the account page inside the base layout wrapper (with navbar etc).
return (
<UIPage brew={brew}>
{renderAccountPage()}
</UIPage>);
};
renderUiItems : function() {
return <>
<div className='dataGroup'>
<h1>Account Information <i className='fas fa-user'></i></h1>
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
</div>
<div className='dataGroup'>
<h3>Homebrewery Information <NaturalCritIcon /></h3>
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
</div>
<div className='dataGroup'>
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
{this.props.uiItems.googleId &&
<p>
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
</p>
}
</div>
<div className='dataGroup'>
<h4>Default Save Location</h4>
{this.renderButton('Homebrewery', 'HOMEBREWERY')}
{this.renderButton('Google Drive', 'GOOGLE-DRIVE', this.state.uiItems.googleId)}
</div>
</>;
},
render : function(){
return <UIPage brew={this.props.brew}>
{this.renderUiItems()}
</UIPage>;
}
});
module.exports = AccountPage;

View File

@@ -0,0 +1,201 @@
require('./archivePage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
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 BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const request = require('../../utils/request-middleware.js');
const ArchivePage = createClass({
displayName : 'ArchivePage',
getDefaultProps : function () {
return {};
},
getInitialState : function () {
return {
title : this.props.query.title || '',
brewCollection : null,
page : 1,
totalPages : 1,
searching : false,
error : null,
};
},
componentDidMount : function() {
},
handleChange(e) {
this.setState({ title: e.target.value });
},
updateStateWithBrews : function (brews, page, totalPages) {
this.setState({
brewCollection : brews || null,
page : page || 1,
totalPages : totalPages || 1,
searching : false
});
},
loadPage : async function(page) {
if(this.state.title == '') {} else {
try {
//this.updateUrl();
this.setState({ searching: true, error: null });
const title = encodeURIComponent(this.state.title);
await request.get(`/api/archive?title=${title}&page=${page}`)
.then((response)=>{
if(response.ok) {
this.updateStateWithBrews(response.body.brews, page, response.body.totalPages);
}
});
} catch (error) {
console.log(`LoadPage error: ${error}`);
}
}
},
updateUrl : function() {
const url = new URL(window.location.href);
const urlParams = new URLSearchParams(url.search);
// Set the title and page parameters
urlParams.set('title', this.state.title);
urlParams.set('page', this.state.page);
url.search = urlParams.toString(); // Convert URLSearchParams to string
window.history.replaceState(null, null, url);
},
renderFoundBrews() {
const { title, brewCollection, page, totalPages, error } = this.state;
if(title === '') {return (<div className='foundBrews noBrews'><h3>Whenever you want, just start typing...</h3></div>);}
if(error !== null) {
return (
<div className='foundBrews noBrews'>
<div><h3>I'm sorry, your request didn't work</h3>
<br /><p>Your search is not specific enough. Too many brews meet this criteria for us to display them.</p>
</div></div>
);
}
if(!brewCollection || brewCollection.length === 0) {
return (
<div className='foundBrews noBrews'>
<h3>We haven't found brews meeting your request.</h3>
</div>
);
}
return (
<div className='foundBrews'>
<span className='brewCount'>{`Brews Found: ${brewCollection.length}`}</span>
{brewCollection.map((brew, index)=>(
<BrewItem brew={brew} key={index} reportError={this.props.reportError} />
))}
<div className='paginationControls'>
{page > 1 && (
<button onClick={()=>this.loadPage(page - 1)}>Previous Page</button>
)}
<span className='currentPage'>Page {page}</span>
{page < totalPages && (
<button onClick={()=>this.loadPage(page + 1)}>Next Page</button>
)}
</div>
</div>
);
},
renderForm : function () {
return (
<div className='brewLookup'>
<h2>Brew Lookup</h2>
<label>Title of the brew</label>
<input
type='text'
value={this.state.title}
onChange={this.handleChange}
onKeyDown={(e)=>{
if(e.key === 'Enter') {
this.handleChange(e);
this.loadPage(1);
}
}}
placeholder='v3 Reference Document'
/>
{/* In the future, we should be able to filter the results by adding tags.
<label>Tags</label><input type='text' value={this.state.query} placeholder='add a tag to filter'/>
<input type="checkbox" id="v3" /><label>v3 only</label>
*/}
<button onClick={()=>{ this.handleChange({ target: { value: this.state.title } }); this.loadPage(1); }}>
<i
className={cx('fas', {
'fa-search' : !this.state.searching,
'fa-spin fa-spinner' : this.state.searching,
})}
/>
</button>
</div>
);
},
renderNavItems : function () {
return (
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>Archive: Search for brews</Nav.item>
</Nav.section>
<Nav.section>
<NewBrew />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>
);
},
render : function () {
return (
<div className='archivePage'>
<link href='/themes/V3/Blank/style.css' rel='stylesheet'/>
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
{this.renderNavItems()}
<div className='content'>
<div className='welcome'>
<h1>Welcome to the Archive</h1>
</div>
<div className='flexGroup'>
<div className='form dataGroup'>{this.renderForm()}</div>
<div className='resultsContainer dataGroup'>
<div className='title'>
<h2>Your results, my lordship</h2>
</div>
{this.renderFoundBrews()}
</div>
</div>
</div>
</div>
);
},
});
module.exports = ArchivePage;

View File

@@ -0,0 +1,173 @@
body {
height: 100vh;
.content {
height: 100%;
}
}
.archivePage {
overflow-y: hidden;
height: 100%;
background-color: #2C3E50;
h1,h2,h3 {
font-family: 'Open Sans';
color: white;
font-weight: 900;
}
.content {
display: grid;
grid-template-rows: 20vh 1fr;
.welcome {
display: grid;
place-items: center;
background: url('https://i.imgur.com/MJ4YHu7.jpg');
background-size: 100%;
background-position: center;
height: 20vh;
border-bottom: 5px solid #333;
h1 {
font-size: 40px;
filter:drop-shadow(0 0 5px black);
}
}
.flexGroup {
height: 100%;
display: grid;
grid-template-columns: 500px 2fr;
background: #2C3E50;
.dataGroup {
width: 100%;
height: 100%;
background: white;
&.form .brewLookup {
padding: 50px;
h2 {
font-size: 30px;
border-bottom: 2px solid;
margin-block: 20px;
}
label {
margin-right: 10px;
}
input+button {
margin-left: 20px;
}
}
&.resultsContainer {
display: flex;
flex-direction: column;
border-left: 2px solid;
height: 100%;
font-family: "BookInsanityRemake";
font-size: .34cm;
.title {
height: 10vh;
background-color: #333;
display: grid;
place-items: center;
h2 {
font-size: 30px;
}
}
.foundBrews {
position: relative;
background-color: #2C3E50;
width: 100%;
max-height: 100%;
height: 66.7vh;
padding: 50px;
overflow-y:scroll;
h3 {
font-size: 25px;
}
&.noBrews {
display:grid;
place-items:center;
}
.brewCount {
position: fixed;
bottom: 0;
right: 17px;
font-size: 11px;
font-weight: 800;
color: white;
background-color: #333;
padding: 8px 10px;
z-index: 1000;
font-family: 'Open Sans';
&:empty {
display: none;
}
}
.limit {
position: fixed;
bottom: 0;
left: 502px;
font-size: 11px;
font-weight: 800;
color: white;
background-color: #333;
padding: 8px 10px;
z-index: 1000;
font-family: 'Open Sans';
&:empty {
display: none;
}
}
.brewItem {
background-image: url('/assets/parchmentBackground.jpg');
width: 48%;
margin-right: 40px;
color: black;
&:nth-child(even) {
margin-right: 0;
}
h2 {
font-size: 0.75cm;
line-height: 0.988em;
font-family: "MrEavesRemake";
font-weight: 800;
color: var(--HB_Color_HeaderText);
}
.info {
font-family: ScalySansRemake;
font-size: 1.2em;
>span {
margin-right: 12px;
line-height: 1.5em;
}
}
}
hr {
visibility: hidden;
}
}
}
}
}
}
}

View File

@@ -20,8 +20,7 @@ const BrewItem = createClass({
authors : [],
stubbed : true
},
updateListFilter : ()=>{},
reportError : ()=>{}
reportError : ()=>{}
};
},
@@ -45,10 +44,6 @@ const BrewItem = createClass({
});
},
updateFilter : function(type, term){
this.props.updateListFilter(type, term);
},
renderDeleteBrewLink : function(){
if(!this.props.brew.editId) return;
@@ -114,11 +109,10 @@ const BrewItem = createClass({
const brew = this.props.brew;
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
brew.tags.sort((a, b)=>{
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
});
}
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
const authors = brew.authors.length > 0 ? brew.authors : 'No authors';
return <div className='brewItem'>
{brew.thumbnail &&
@@ -137,17 +131,24 @@ const BrewItem = createClass({
<i className='fas fa-tags'/>
{brew.tags.map((tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
return <span key={idx} className={matches[1]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
return <span key={idx} className={matches[1]}>{matches[2]}</span>;
})}
</div>
</> : <></>
}
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
<>
<a key={index} href={`/user/${author}`}>{author}</a>
{index < brew.authors.length - 1 && ', '}
</>))}
<i className='fas fa-user'/> {Array.isArray(authors) ? (
<span>
{authors.map((author, index) => (
<span key={index}>
<a href={`/share/${author}`}>{author}</a>
{index < authors.length - 1 && ', '}
</span>
))}
</span>
) : (
<span>{authors}</span>
)}
</span>
<br />
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>

View File

@@ -63,41 +63,6 @@
white-space: nowrap;
display: inline-block;
font-weight: bold;
border-color: currentColor;
cursor : pointer;
&:before {
font-family: 'Font Awesome 5 Free';
font-size: 12px;
margin-right: 3px;
}
&.type {
background-color: #0080003b;
color: #008000;
&:before{
content: '\f0ad';
}
}
&.group {
background-color: #5050503b;
color: #000000;
&:before{
content: '\f500';
}
}
&.meta {
background-color: #0000803b;
color: #000080;
&:before{
content: '\f05a';
}
}
&.system {
background-color: #8000003b;
color: #800000;
&:before{
content: '\f518';
}
}
}
&:hover{
.links{

View File

@@ -36,7 +36,6 @@ const ListPage = createClass({
return {
filterString : this.props.query?.filter || '',
filterTags : [],
sortType : this.props.query?.sort || null,
sortDir : this.props.query?.dir || null,
query : this.props.query,
@@ -83,7 +82,7 @@ const ListPage = createClass({
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
return _.map(brews, (brew, idx)=>{
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError} updateListFilter={ (tag)=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}/>;
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
});
},
@@ -137,33 +136,13 @@ const ListPage = createClass({
return;
},
updateUrl : function(filterTerm, sortType, sortDir, filterTag=''){
updateUrl : function(filterTerm, sortType, sortDir){
const url = new URL(window.location.href);
const urlParams = new URLSearchParams(url.search);
urlParams.set('sort', sortType);
urlParams.set('dir', sortDir);
let filterTags = urlParams.getAll('tag');
if(filterTag != '') {
if(filterTags.findIndex((tag)=>{return tag.toLowerCase()==filterTag.toLowerCase();}) == -1){
filterTags.push(filterTag);
} else {
filterTags = filterTags.filter((tag)=>{ return tag.toLowerCase() != filterTag.toLowerCase(); });
}
}
urlParams.delete('tag');
// Add tags to URL in the order they were clicked
filterTags.forEach((tag)=>{ urlParams.append('tag', tag); });
// Sort tags before updating state
filterTags.sort((a, b)=>{
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
});
this.setState({
filterTags
});
if(!filterTerm)
urlParams.delete('filter');
else
@@ -187,16 +166,6 @@ const ListPage = createClass({
</div>;
},
renderTagsOptions : function(){
if(this.state.filterTags?.length == 0) return;
return <div className='tags-container'>
{_.map(this.state.filterTags, (tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
return <span key={idx} className={matches[1]} onClick={()=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}>{matches[2]}</span>;
})}
</div>;
},
renderSortOptions : function(){
return <div className='sort-container'>
<h6>Sort by :</h6>
@@ -207,6 +176,9 @@ const ListPage = createClass({
{/* {this.renderSortOption('Latest', 'latest')} */}
{this.renderFilterOption()}
</div>;
},
@@ -214,28 +186,14 @@ const ListPage = createClass({
const testString = _.deburr(this.state.filterString).toLowerCase();
brews = _.filter(brews, (brew)=>{
// Filter by user entered text
const brewStrings = _.deburr([
brew.title,
brew.description,
brew.tags].join('\n')
.toLowerCase());
const filterTextTest = brewStrings.includes(testString);
// Filter by user selected tags
let filterTagTest = true;
if(this.state.filterTags.length > 0){
filterTagTest = Array.isArray(brew.tags) && this.state.filterTags?.every((tag)=>{
return brew.tags.findIndex((brewTag)=>{
return brewTag.toLowerCase() == tag.toLowerCase();
}) >= 0;
});
}
return filterTextTest && filterTagTest;
return brewStrings.includes(testString);
});
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
},
@@ -262,11 +220,10 @@ const ListPage = createClass({
render : function(){
return <div className='listPage sitePage'>
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
<link href='/themes/V3/Blank/style.css' type="text/css" rel='stylesheet'/>
<link href='/themes/V3/5ePHB/style.css' type="text/css" rel='stylesheet'/>
<link href='/themes/V3/Blank/style.css' rel='stylesheet'/>
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
{this.props.navItems}
{this.renderSortOptions()}
{this.renderTagsOptions()}
<div className='content V3'>
<div className='page'>

View File

@@ -53,7 +53,7 @@
}
}
}
.sort-container {
.sort-container{
font-family : 'Open Sans', sans-serif;
position : sticky;
top : 0;
@@ -125,66 +125,4 @@
}
.tags-container {
height : 30px;
background-color : #555;
border-top : 1px solid #666;
border-bottom : 1px solid #666;
color : white;
display : flex;
justify-content : center;
align-items : center;
column-gap : 15px;
row-gap : 5px;
flex-wrap : wrap;
span {
font-family : 'Open Sans', sans-serif;
font-size : 11px;
font-weight : bold;
border : 1px solid;
border-radius : 3px;
padding : 3px;
cursor : pointer;
color: #dfdfdf;
&:before {
font-family: 'Font Awesome 5 Free';
font-size: 12px;
margin-right: 3px;
}
&:after {
content: '\f00d';
font-family: 'Font Awesome 5 Free';
font-size: 12px;
margin-left: 3px;
}
&.type {
background-color: #008000;
border-color: #00a000;
&:before{
content: '\f0ad';
}
}
&.group {
background-color: #505050;
border-color: #000000;
&:before{
content: '\f500';
}
}
&.meta {
background-color: #000080;
border-color: #0000a0;
&:before{
content: '\f05a';
}
}
&.system {
background-color: #800000;
border-color: #a00000;
&:before{
content: '\f518';
}
}
}
}
}

View File

@@ -113,7 +113,7 @@ const EditPage = createClass({
brew : { ...prevState.brew, text: text },
isPending : true,
htmlErrors : htmlErrors,
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
currentEditorPage : this.refs.editor.getCurrentPage()
}), ()=>{if(this.state.autoSave) this.trySave();});
},

View File

@@ -73,11 +73,9 @@ const errorIndex = (props)=>{
**Properties** tab, and adding your username to the "invited authors" list. You can
then try to access this document again.
:
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
@@ -88,14 +86,9 @@ const errorIndex = (props)=>{
You must be logged in to one of the accounts listed as an author of this brew.
User is not logged in. Please log in [here](${loginUrl}).
:
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
// Brew load error
'05' : dedent`
@@ -104,8 +97,6 @@ const errorIndex = (props)=>{
The server could not locate the Homebrewery document. It was likely deleted by
its owner.
:
**Requested access:** ${props.brew.accessType}
**Brew ID:** ${props.brew.brewId}`,
@@ -122,8 +113,6 @@ const errorIndex = (props)=>{
An error occurred while attempting to remove the Homebrewery document.
:
**Brew ID:** ${props.brew.brewId}`,
// Author delete error
@@ -132,21 +121,7 @@ const errorIndex = (props)=>{
An error occurred while attempting to remove the user from the Homebrewery document author list!
:
**Brew ID:** ${props.brew.brewId}`,
// Brew locked by Administrators error
'100' : dedent`
## This brew has been locked.
Please contact the Administrators to unlock this document.
:
**Brew ID:** ${props.brew.brewId}
**Brew Title:** ${props.brew.brewTitle}`,
};
};

View File

@@ -31,10 +31,9 @@ const HomePage = createClass({
},
getInitialState : function() {
return {
brew : this.props.brew,
welcomeText : this.props.brew.text,
error : undefined,
currentEditorPage : 0
brew : this.props.brew,
welcomeText : this.props.brew.text,
error : undefined
};
},
handleSave : function(){
@@ -54,8 +53,7 @@ const HomePage = createClass({
},
handleTextChange : function(text){
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
brew : { ...prevState.brew, text: text }
}));
},
renderNavbar : function(){
@@ -87,12 +85,7 @@ const HomePage = createClass({
renderer={this.state.brew.renderer}
showEditButtons={false}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
currentEditorPage={this.state.currentEditorPage}
/>
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
</SplitPane>
</div>

View File

@@ -143,7 +143,6 @@ Much nicer than `<br><br><br><br><br>`
### Column Breaks
Column and page breaks with `\column` and `\page`.
\column
### Tables
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.

View File

@@ -38,12 +38,11 @@ const NewPage = createClass({
const brew = this.props.brew;
return {
brew : brew,
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
error : null,
htmlErrors : Markdown.validate(brew.text),
currentEditorPage : 0
brew : brew,
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
error : null,
htmlErrors : Markdown.validate(brew.text)
};
},
@@ -105,9 +104,8 @@ const NewPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors,
currentEditorPage : this.refs.editor.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors
}));
localStorage.setItem(BREWKEY, text);
},
@@ -222,15 +220,7 @@ const NewPage = createClass({
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage}
/>
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} lang={this.state.brew.lang} errors={this.state.htmlErrors}/>
</SplitPane>
</div>
</div>;

View File

@@ -21,8 +21,7 @@ const PrintPage = createClass({
brew : {
text : '',
style : '',
renderer : 'legacy',
lang : ''
renderer : 'legacy'
}
};
},
@@ -33,8 +32,7 @@ const PrintPage = createClass({
text : this.props.brew.text || '',
style : this.props.brew.style || undefined,
renderer : this.props.brew.renderer || 'legacy',
theme : this.props.brew.theme || '5ePHB',
lang : this.props.brew.lang || 'en'
theme : this.props.brew.theme || '5ePHB'
}
};
},
@@ -51,8 +49,7 @@ const PrintPage = createClass({
text : brewStorage,
style : styleStorage,
renderer : metaStorage?.renderer || 'legacy',
theme : metaStorage?.theme || '5ePHB',
lang : metaStorage?.lang || 'en'
theme : metaStorage?.theme || '5ePHB'
}
};
});
@@ -96,14 +93,14 @@ const PrintPage = createClass({
return <div>
<Meta name='robots' content='noindex, nofollow' />
<link href={`/themes/${rendererPath}/Blank/style.css`} type="text/css" rel='stylesheet'/>
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type="text/css" rel='stylesheet'/>
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
}
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type="text/css" rel='stylesheet'/>
<link href={`/themes/${rendererPath}/${themePath}/style.css`} rel='stylesheet'/>
{/* Apply CSS from Style tab */}
{this.renderStyle()}
<div className='pages' ref='pages' lang={this.state.brew.lang}>
<div className='pages' ref='pages'>
{this.renderPages()}
</div>
</div>;

View File

@@ -12,9 +12,9 @@ const template = async function(name, title='', props = {}){
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
${ogMetaTags}
<meta name="twitter:card" content="summary">

2041
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.12.0",
"version": "3.10.0",
"engines": {
"npm": "^10.2.x",
"node": "^20.8.x"
@@ -26,12 +26,10 @@
"test:coverage": "jest --coverage --silent --runInBand",
"test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
"test:variables": "jest tests/markdown/variables.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:definition-lists": "jest tests/markdown/definition-lists.test.js --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",
@@ -81,19 +79,18 @@
]
},
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/plugin-transform-runtime": "^7.24.3",
"@babel/preset-env": "^7.24.4",
"@babel/preset-react": "^7.24.1",
"@googleapis/drive": "^8.7.0",
"@babel/core": "^7.23.9",
"@babel/plugin-transform-runtime": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/preset-react": "^7.23.3",
"@googleapis/drive": "^8.6.0",
"body-parser": "^1.20.2",
"classnames": "^2.5.1",
"classnames": "^2.3.2",
"codemirror": "^5.65.6",
"cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
"expr-eval": "^2.0.2",
"express": "^4.19.2",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7",
"fs-extra": "11.2.0",
@@ -101,32 +98,32 @@
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "11.2.0",
"marked": "11.1.1",
"marked-extended-tables": "^1.0.8",
"marked-gfm-heading-id": "^3.1.3",
"marked-gfm-heading-id": "^3.1.2",
"marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.3.1",
"mongoose": "^8.1.1",
"nanoid": "3.3.4",
"nconf": "^0.12.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-frame-component": "^4.1.3",
"react-router-dom": "6.22.3",
"react-router-dom": "6.21.3",
"sanitize-filename": "1.6.3",
"superagent": "^8.1.2",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.2.0",
"eslint-plugin-react": "^7.34.1",
"eslint": "^8.56.0",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-react": "^7.33.2",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0",
"stylelint": "^15.11.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recess-order": "^4.4.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3",
"supertest": "^6.3.4"

View File

@@ -7,14 +7,6 @@ DB.connect(config).then(()=>{
// before launching server
const PORT = process.env.PORT || config.get('web_port') || 8000;
server.app.listen(PORT, ()=>{
const reset = '\x1b[0m'; // Reset to default style
const bright = '\x1b[1m'; // Bright (bold) style
const cyan = '\x1b[36m'; // Cyan color
const underline = '\x1b[4m'; // Underlined style
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
console.log(`\tserver on port: ${PORT}`);
console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`)
console.log(`server on port: ${PORT}`);
});
});

View File

@@ -84,7 +84,7 @@ router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{
return res.status(500).json({ error: 'Internal Server Error' });
});
});
/* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
const query = uncompressedBrewQuery.clone();

View File

@@ -17,8 +17,21 @@ const asyncHandler = require('express-async-handler');
const { DEFAULT_BREW } = require('./brewDefaults.js');
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```metadata')) {
const index = brew.text.indexOf('```\n\n');
const metadataSection = brew.text.slice(12, index - 1);
const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
brew.text = brew.text.slice(index + 5);
}
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
};
const sanitizeBrew = (brew, accessType)=>{
brew._id = undefined;
@@ -54,6 +67,7 @@ app.use((req, res, next)=>{
app.use(homebrewApi);
app.use(require('./admin.api.js'));
app.use(require('./archive.api.js'));
const HomebrewModel = require('./homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
@@ -372,7 +386,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
console.log(err);
});
data.accountDetails = {
data.uiItems = {
username : req.account.username,
issued : req.account.issued,
googleId : Boolean(req.account.googleId),
@@ -468,6 +482,11 @@ app.use(async (err, req, res, next)=>{
res.status(err.status || err.response?.status || 500).send(err);
return;
}
if(err.originalUrl?.startsWith('/archive/')) {
// console.log('archive error');
res.status(err.status || err.response?.status || 500).send(err);
return;
}
// console.log('non-API error');
const status = err.status || err.code || 500;
@@ -491,6 +510,8 @@ app.use(async (err, req, res, next)=>{
res.send(page);
});
app.use((req, res)=>{
if(!res.headersSent) {
console.error('Headers have not been sent, responding with a server error.', req.url);

53
server/archive.api.js Normal file
View File

@@ -0,0 +1,53 @@
const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
const asyncHandler = require('express-async-handler');
const archive = {
archiveApi : router,
/* Searches for matching title, also attempts to partial match */
findBrews : async (req, res, next)=>{
try {
const title = req.query.title || '';
const page = parseInt(req.query.page) || 1;
console.log('try:', page);
const pageSize = 10; // Set a default page size
const skip = (page - 1) * pageSize;
const titleQuery = {
title : { $regex: decodeURIComponent(title), $options: 'i' },
published : true
};
const projection = {
editId : 0,
googleId : 0,
text : 0,
textBin : 0,
};
const brews = await HomebrewModel.find(titleQuery, projection)
.skip(skip)
.limit(pageSize)
.maxTimeMS(5000)
.exec();
if(!brews || brews.length === 0) {
// No published documents found with the given title
return res.status(404).json({ error: 'Published documents not found' });
}
const totalDocuments = await HomebrewModel.countDocuments(title);
const totalPages = Math.ceil(totalDocuments / pageSize);
return res.json({ brews, page, totalPages });
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Internal Server Error' });
}
}
};
router.get('/api/archive', asyncHandler(archive.findBrews));
module.exports = router;

View File

@@ -7,9 +7,7 @@ const config = require('./config.js');
let serviceAuth;
if(!config.get('service_account')){
const reset = '\x1b[0m'; // Reset to default style
const yellow = '\x1b[33m'; // yellow color
console.warn(`\n${yellow}No Google Service Account in config files - Google Drive integration will not be available.${reset}`);
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
} else {
const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) :
@@ -20,7 +18,7 @@ if(!config.get('service_account')){
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
} catch (err) {
console.warn(err);
console.warn('Please make sure the Google Service Account is set up properly in your config files.');
console.log('Please make sure the Google Service Account is set up properly in your config files.');
}
}

View File

@@ -54,10 +54,6 @@ const api = {
});
stub = stub?.toObject();
if(stub?.lock?.locked && accessType != 'edit') {
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.message, brewId: stub.shareId, brewTitle: stub.title };
}
// If there is a google id, try to find the google brew
if(!stubOnly && (googleId || stub?.googleId)) {
let googleError;
@@ -83,9 +79,9 @@ const api = {
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
const accessError = { name: 'Access Error', status: 401 };
if(req.account){
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
}
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId};
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
}
// If after all of that we still don't have a brew, throw an exception

View File

@@ -117,7 +117,7 @@ describe('Tests for api', ()=>{
id : '123456789012345678901234567890123abcdefghijkl'
}
});
expect(googleId).toEqual('123456789012345678901234567890123');
expect(id).toEqual('abcdefghijkl');
});
@@ -128,7 +128,7 @@ describe('Tests for api', ()=>{
id : '123456789012345678901234567890123abcdefghij'
}
});
expect(googleId).toEqual('123456789012345678901234567890123');
expect(id).toEqual('abcdefghij');
});
@@ -298,18 +298,6 @@ describe('Tests for api', ()=>{
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
});
it('access is denied to a locked brew', async()=>{
const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, message: 'brew locked' } };
model.get = jest.fn(()=>toBrewPromise(lockBrew));
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
const fn = api.getBrew('share', false);
const req = { brew: {} };
const next = jest.fn();
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
});
});
describe('mergeBrewText', ()=>{

View File

@@ -1,22 +0,0 @@
const _ = require('lodash');
const yaml = require('js-yaml');
const splitTextStyleAndMetadata = (brew) => {
brew.text = brew.text.replaceAll('\r\n', '\n');
if (brew.text.startsWith('```metadata')) {
const index = brew.text.indexOf('```\n\n');
const metadataSection = brew.text.slice(12, index - 1);
const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
brew.text = brew.text.slice(index + 5);
}
if (brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
};
module.exports = {
splitTextStyleAndMetadata
};

View File

@@ -436,7 +436,7 @@ const CodeEditor = createClass({
render : function(){
return <>
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type="text/css" rel='stylesheet' />
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} rel='stylesheet' />
<div className='codeEditor' ref='editor' style={this.props.style}/>
</>;
}

View File

@@ -17,23 +17,13 @@
text-shadow: none;
font-weight: 600;
color: grey;
}
}
.sourceMoveFlash .CodeMirror-line{
animation-name: sourceMoveAnimation;
animation-duration: 0.4s;
}
.CodeMirror-vscrollbar {
&::-webkit-scrollbar {
width: 20px;
}
&::-webkit-scrollbar-thumb {
width: 20px;
background: linear-gradient(90deg, #858585 15px, #808080 15px);
}
}
//.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
//}

View File

@@ -4,40 +4,7 @@ const Marked = require('marked');
const MarkedExtendedTables = require('marked-extended-tables');
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
const MathParser = require('expr-eval').Parser;
const renderer = new Marked.Renderer();
const tokenizer = new Marked.Tokenizer();
//Limit math features to simple items
const mathParser = new MathParser({
operators : {
// These default to true, but are included to be explicit
add : true,
subtract : true,
multiply : true,
divide : true,
power : true,
round : true,
floor : true,
ceil : true,
sin : false, cos : false, tan : false, asin : false, acos : false,
atan : false, sinh : false, cosh : false, tanh : false, asinh : false,
acosh : false, atanh : false, sqrt : false, cbrt : false, log : false,
log2 : false, ln : false, lg : false, log10 : false, expm1 : false,
log1p : false, abs : false, trunc : false, join : false, sum : false,
'-' : false, '+' : false, exp : false, not : false, length : false,
'!' : false, sign : false, random : false, fac : false, min : false,
max : false, hypot : false, pyt : false, pow : false, atan2 : false,
'if' : false, gamma : false, roundTo : false, map : false, fold : false,
filter : false, indexOf : false,
remainder : false, factorial : false,
comparison : false, concatenate : false,
logical : false, assignment : false,
array : false, fndef : false
}
});
//Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) {
@@ -83,11 +50,6 @@ renderer.link = function (href, title, text) {
return out;
};
// Disable default reflink behavior, as it steps on our variables extension
tokenizer.def = function () {
return undefined;
};
const mustacheSpans = {
name : 'mustacheSpans',
level : 'inline', // Is this a block-level or inline-level tokenizer?
@@ -294,10 +256,10 @@ const superSubScripts = {
}
};
const definitionListsSingleLine = {
name : 'definitionListsSingleLine',
const definitionLists = {
name : 'definitionLists',
level : 'block',
start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
start(src) { return src.match(/^.*?::.*/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 +274,7 @@ const definitionListsSingleLine = {
}
if(definitions.length) {
return {
type : 'definitionListsSingleLine',
type : 'definitionLists',
raw : src.slice(0, endIndex),
definitions
};
@@ -326,300 +288,9 @@ const definitionListsSingleLine = {
}
};
const definitionListsMultiLine = {
name : 'definitionListsMultiLine',
level : 'block',
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;
let endIndex = 0;
const definitions = [];
while (match = regex.exec(src)) {
if(match[1]) {
if(this.lexer.blockTokens(match[1].trim())[0]?.type !== 'paragraph') // DT must not be another block-level token besides <p>
break;
definitions.push({
dt : this.lexer.inlineTokens(match[1].trim()),
dds : []
});
}
if(match[2] && definitions.length) {
definitions[definitions.length - 1].dds.push(
this.lexer.inlineTokens(match[2].trim().replace(/\s/g, ' '))
);
}
endIndex = regex.lastIndex;
}
if(definitions.length) {
return {
type : 'definitionListsMultiLine',
raw : src.slice(0, endIndex),
definitions
};
}
},
renderer(token) {
let returnVal = `<dl>`;
token.definitions.forEach((def)=>{
const dds = def.dds.map((s)=>{
return `\n<dd>${this.parser.parseInline(s).trim()}</dd>`;
}).join('');
returnVal += `<dt>${this.parser.parseInline(def.dt)}</dt>${dds}\n`;
});
returnVal = returnVal.trim();
return `${returnVal}</dl>`;
}
};
//v=====--------------------< Variable Handling >-------------------=====v// 242 lines
const replaceVar = function(input, hoist=false, allowUnresolved=false) {
const regex = /([!$]?)\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
const match = regex.exec(input);
const prefix = match[1];
const label = match[2];
//v=====--------------------< HANDLE MATH >-------------------=====v//
const mathRegex = /[a-z]+\(|[+\-*/^()]/g;
const matches = label.split(mathRegex);
const mathVars = matches.filter((match)=>isNaN(match))?.map((s)=>s.trim()); // Capture any variable names
let replacedLabel = label;
if(prefix[0] == '$' && mathVars?.[0] !== label.trim()) {// If there was mathy stuff not captured, let's do math!
mathVars?.forEach((variable)=>{
const foundVar = lookupVar(variable, globalPageNumber, hoist);
if(foundVar && foundVar.resolved && foundVar.content && !isNaN(foundVar.content)) // Only subsitute math values if fully resolved, not empty strings, and numbers
replacedLabel = replacedLabel.replaceAll(variable, foundVar.content);
});
try {
return mathParser.evaluate(replacedLabel);
} catch (error) {
return undefined; // Return undefined if invalid math result
}
}
//^=====--------------------< HANDLE MATH >-------------------=====^//
const foundVar = lookupVar(label, globalPageNumber, hoist);
if(!foundVar || (!foundVar.resolved && !allowUnresolved))
return undefined; // Return undefined if not found, or parially-resolved vars are not allowed
// url or <url> "title" or 'title' or (title)
const linkRegex = /^([^<\s][^\s]*|<.*?>)(?: ("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\((?:\\\(|\\\)|[^()])*\)))?$/m;
const linkMatch = linkRegex.exec(foundVar.content);
const href = linkMatch ? linkMatch[1] : null; //TODO: TRIM OFF < > IF PRESENT
const title = linkMatch ? linkMatch[2]?.slice(1, -1) : null;
if(!prefix[0] && href) // Link
return `[${label}](${href}${title ? ` "${title}"` : ''})`;
if(prefix[0] == '!' && href) // Image
return `![${label}](${href} ${title ? ` "${title}"` : ''})`;
if(prefix[0] == '$') // Variable
return foundVar.content;
};
const lookupVar = function(label, index, hoist=false) {
while (index >= 0) {
if(globalVarsList[index]?.[label] !== undefined)
return globalVarsList[index][label];
index--;
}
if(hoist) { //If normal lookup failed, attempt hoisting
index = Object.keys(globalVarsList).length; // Move index to start from last page
while (index >= 0) {
if(globalVarsList[index]?.[label] !== undefined)
return globalVarsList[index][label];
index--;
}
}
return undefined;
};
const processVariableQueue = function() {
let resolvedOne = true;
let finalLoop = false;
while (resolvedOne || finalLoop) { // Loop through queue until no more variable calls can be resolved
resolvedOne = false;
for (const item of varsQueue) {
if(item.type == 'text')
continue;
if(item.type == 'varDefBlock') {
const regex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]/g;
let match;
let resolved = true;
let tempContent = item.content;
while (match = regex.exec(item.content)) { // regex to find variable calls
const value = replaceVar(match[0], true);
if(value == undefined)
resolved = false;
else
tempContent = tempContent.replaceAll(match[0], value);
}
if(resolved == true || item.content != tempContent) {
resolvedOne = true;
item.content = tempContent;
}
globalVarsList[globalPageNumber][item.varName] = {
content : item.content,
resolved : resolved
};
if(resolved)
item.type = 'resolved';
}
if(item.type == 'varCallBlock' || item.type == 'varCallInline') {
const value = replaceVar(item.content, true, finalLoop); // final loop will just use the best value so far
if(value == undefined)
continue;
resolvedOne = true;
item.content = value;
item.type = 'text';
}
}
varsQueue = varsQueue.filter((item)=>item.type !== 'resolved'); // Remove any fully-resolved variable definitions
if(finalLoop)
break;
if(!resolvedOne)
finalLoop = true;
}
varsQueue = varsQueue.filter((item)=>item.type !== 'varDefBlock');
};
function MarkedVariables() {
return {
hooks : {
preprocess(src) {
const codeBlockSkip = /^(?: {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+|^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})(?:[^\n]*)(?:\n|$)(?:|(?:[\s\S]*?)(?:\n|$))(?: {0,3}\2[~`]* *(?=\n|$))|`[^`]*?`/;
const blockDefRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\]:(?!\() *((?:\n? *[^\s].*)+)(?=\n+|$)/; //Matches 3, [4]:5
const blockCallRegex = /^[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?=\n|$)/; //Matches 6, [7]
const inlineDefRegex = /([!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\])\(([^\n]+)\)/; //Matches 8, 9[10](11)
const inlineCallRegex = /[!$]?\[((?!\s*\])(?:\\.|[^\[\]\\])+)\](?!\()/; //Matches 12, [13]
// Combine regexes and wrap in parens like so: (regex1)|(regex2)|(regex3)|(regex4)
const combinedRegex = new RegExp([codeBlockSkip, blockDefRegex, blockCallRegex, inlineDefRegex, inlineCallRegex].map((s)=>`(${s.source})`).join('|'), 'gm');
let lastIndex = 0;
let match;
while ((match = combinedRegex.exec(src)) !== null) {
// Format any matches into tokens and store
if(match.index > lastIndex) { // Any non-variable stuff
varsQueue.push(
{ type : 'text',
varName : null,
content : src.slice(lastIndex, match.index)
});
}
if(match[1]) {
varsQueue.push(
{ type : 'text',
varName : null,
content : match[0]
});
}
if(match[3]) { // Block Definition
const label = match[4] ? match[4].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
const content = match[5] ? match[5].trim().replace(/[ \t]+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
varsQueue.push(
{ type : 'varDefBlock',
varName : label,
content : content
});
}
if(match[6]) { // Block Call
const label = match[7] ? match[7].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
varsQueue.push(
{ type : 'varCallBlock',
varName : label,
content : match[0]
});
}
if(match[8]) { // Inline Definition
const label = match[10] ? match[10].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
let content = match[11] ? match[11].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
// In case of nested (), find the correct matching end )
let level = 0;
let i;
for (i = 0; i < content.length; i++) {
if(content[i] === '\\') {
i++;
} else if(content[i] === '(') {
level++;
} else if(content[i] === ')') {
level--;
if(level < 0)
break;
}
}
if(i > -1) {
combinedRegex.lastIndex = combinedRegex.lastIndex - (content.length - i);
content = content.slice(0, i).trim().replace(/\s+/g, ' ');
}
varsQueue.push(
{ type : 'varDefBlock',
varName : label,
content : content
});
varsQueue.push(
{ type : 'varCallInline',
varName : label,
content : match[9]
});
}
if(match[12]) { // Inline Call
const label = match[13] ? match[13].trim().replace(/\s+/g, ' ') : null; // Trim edge spaces and shorten blocks of whitespace to 1 space
varsQueue.push(
{ type : 'varCallInline',
varName : label,
content : match[0]
});
}
lastIndex = combinedRegex.lastIndex;
}
if(lastIndex < src.length) {
varsQueue.push(
{ type : 'text',
varName : null,
content : src.slice(lastIndex)
});
}
processVariableQueue();
const output = varsQueue.map((item)=>item.content).join('');
varsQueue = []; // Must clear varsQueue because custom HTML renderer uses Marked.parse which will preprocess again without clearing the array
return output;
}
}
};
};
//^=====--------------------< Variable Handling >-------------------=====^//
Marked.use(MarkedVariables());
Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, superSubScripts] });
Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use({ renderer: renderer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
const nonWordAndColonTest = /[^\w:]/g;
@@ -698,28 +369,12 @@ const processStyleTags = (string)=>{
`${attributes?.length ? ` ${attributes.join(' ')}` : ''}`;
};
const globalVarsList = {};
let varsQueue = [];
let globalPageNumber = 0;
module.exports = {
marked : Marked,
render : (rawBrewText, pageNumber=1)=>{
globalVarsList[pageNumber] = {}; //Reset global links for current page, to ensure values are parsed in order
varsQueue = []; //Could move into MarkedVariables()
globalPageNumber = pageNumber;
render : (rawBrewText)=>{
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
const opts = Marked.defaults;
rawBrewText = opts.hooks.preprocess(rawBrewText);
const tokens = Marked.lexer(rawBrewText, opts);
Marked.walkTokens(tokens, opts.walkTokens);
const html = Marked.parser(tokens, opts);
return opts.hooks.postprocess(html);
return Marked.parse(rawBrewText);
},
validate : (rawBrewText)=>{

View File

@@ -1,4 +1,4 @@
require('client/homebrew/navbar/navbar.less');
require('./nav.less');
const React = require('react');
const { useState, useRef, useEffect } = React;
const createClass = require('create-react-class');
@@ -104,7 +104,7 @@ const Nav = {
});
return (
<div className={`navDropdownContainer ${props.className ?? ''}`}
<div className={`navDropdownContainer ${props.className}`}
ref={myRef}
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }

View File

@@ -0,0 +1,97 @@
@import '../styles/colors';
@keyframes glideDropDown {
0% {transform : translate(0px, -100%);
opacity : 0;
background-color: #333;}
100% {transform : translate(0px, 0px);
opacity : 1;
background-color: #333;}
}
nav{
background-color : #333;
.navContent{
position : relative;
display : flex;
justify-content : space-between;
z-index : 2;
}
.navSection{
display : flex;
align-items : center;
}
.navLogo{
display : block;
margin-top : 0px;
margin-right : 8px;
margin-left : 8px;
color : white;
text-decoration : none;
&:hover{
.name{ color : @orange; }
svg{ fill : @orange }
}
svg{
height : 13px;
margin-right : 0.2em;
cursor : pointer;
fill : white;
}
span.name{
font-family : 'CodeLight';
font-size : 15px;
span.crit{
font-family : 'CodeBold';
}
small{
font-family : 'Open Sans';
font-size : 0.3em;
font-weight : 800;
text-transform : uppercase;
}
}
}
.navItem{
#backgroundColorsHover;
.animate(background-color);
padding : 8px 12px;
cursor : pointer;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
text-decoration : none;
text-transform : uppercase;
line-height : 13px;
i{
margin-left : 5px;
font-size : 13px;
float : right;
}
}
.navSection:last-child .navItem{
border-left : 1px solid #666;
}
.navDropdownContainer{
position: relative;
.navDropdown {
position : absolute;
top : 28px;
left : 0px;
z-index : 10000;
width : 100%;
overflow : hidden auto;
max-height : calc(100vh - 28px);
.navItem{
animation-name: glideDropDown;
animation-duration: 0.4s;
position : relative;
display : block;
width : 100%;
vertical-align : middle;
padding : 8px 5px;
border : 1px solid #888;
border-bottom : 0;
}
}
}
}

View File

@@ -1,91 +0,0 @@
/* eslint-disable max-lines */
const Markdown = require('naturalcrit/markdown.js');
describe('Inline Definition Lists', ()=>{
test('No Term 1 Definition', function() {
const source = ':: My First Definition\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt></dt><dd>My First Definition</dd>\n</dl>');
});
test('Single Definition Term', function() {
const source = 'My term :: My First Definition\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>My term</dt><dd>My First Definition</dd>\n</dl>');
});
test('Multiple Definition Terms', function() {
const source = 'Term 1::Definition of Term 1\nTerm 2::Definition of Term 2\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Definition of Term 1</dd>\n<dt>Term 2</dt><dd>Definition of Term 2</dd>\n</dl>');
});
});
describe('Multiline Definition Lists', ()=>{
test('Single Term, Single Definition', function() {
const source = 'Term 1\n::Definition 1\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd></dl>');
});
test('Single Term, Plural Definitions', function() {
const source = 'Term 1\n::Definition 1\n::Definition 2\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
});
test('Multiple Term, Single Definitions', function() {
const source = 'Term 1\n::Definition 1\n\nTerm 2\n::Definition 1\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd></dl>');
});
test('Multiple Term, Plural Definitions', function() {
const source = 'Term 1\n::Definition 1\n::Definition 2\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
});
test('Single Term, Single multi-line definition', function() {
const source = 'Term 1\n::Definition 1\nand more and\nmore and more\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more and more</dd></dl>');
});
test('Single Term, Plural multi-line definitions', function() {
const source = 'Term 1\n::Definition 1\nand more and more\n::Definition 2\nand more\nand more\n::Definition 3\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dd>Definition 2 and more and more</dd>\n<dd>Definition 3</dd></dl>');
});
test('Multiple Term, Single multi-line definition', function() {
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl>');
});
test('Multiple Term, Single multi-line definition, followed by an inline dl', function() {
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\n::Inline Definition (no term)';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><dl><dt></dt><dd>Inline Definition (no term)</dd>\n</dl>');
});
test('Multiple Term, Single multi-line definition, followed by paragraph', function() {
const source = 'Term 1\n::Definition 1\nand more and more\n\nTerm 2\n::Definition 1\n::Definition 2\n\nParagraph';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt>\n<dd>Definition 1 and more and more</dd>\n<dt>Term 2</dt>\n<dd>Definition 1</dd>\n<dd>Definition 2</dd></dl><p>Paragraph</p>');
});
test('Block Token cannot be the Term of a multi-line definition', function() {
const source = '## Header\n::Definition 1 of a single-line DL\n::Definition 1 of another single-line DL';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<h2 id="header">Header</h2>\n<dl><dt></dt><dd>Definition 1 of a single-line DL</dd>\n<dt></dt><dd>Definition 1 of another single-line DL</dd>\n</dl>');
});
test('Inline DL has priority over Multiline', function() {
const source = 'Term 1 :: Inline definition 1\n:: Inline definition 2 (no DT)';
const rendered = Markdown.render(source).trim();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<dl><dt>Term 1</dt><dd>Inline definition 1</dd>\n<dt></dt><dd>Inline definition 2 (no DT)</dd>\n</dl>');
});
});

View File

@@ -1,373 +0,0 @@
/* 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, '').trim();
};
renderAllPages = function(pages){
const outputs = [];
pages.forEach((page, index)=>{
const output = Markdown.render(page, index);
outputs.push(output);
});
return outputs;
};
// 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('Block-level variables', ()=>{
it('Handles variable assignment and recall with simple text', function() {
const source = dedent`
[var]: string
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
});
it('Handles variable assignment and recall with multiline string', function() {
const source = dedent`
[var]: string
across multiple
lines
$[var]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string across multiple lines</p>');
});
it('Handles variable assignment and recall with tables', function() {
const source = dedent`
[var]:
##### Title
| H1 | H2 |
|:---|:--:|
| A | B |
| C | D |
$[var]`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<h5 id="title">Title</h5>
<table><thead><tr><th align=left>H1</th>
<th align=center>H2</th>
</tr></thead><tbody><tr><td align=left>A</td>
<td align=center>B</td>
</tr><tr><td align=left>C</td>
<td align=center>D</td>
</tr></tbody></table>`.trimReturns());
});
it('Hoists undefined variables', function() {
const source = dedent`
$[var]
[var]: string`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p>');
});
it('Hoists last instance of variable', function() {
const source = dedent`
$[var]
[var]: string
[var]: new string`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>new string</p>');
});
it('Handles complex hoisting', function() {
const source = dedent`
$[titleAndName]: $[title] $[fullName]
$[title]: Mr.
$[fullName]: $[firstName] $[lastName]
[firstName]: Bob
Welcome, $[titleAndName]!
[lastName]: Jacob
[lastName]: $[lastName]son
`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Welcome, Mr. Bob Jacobson!</p>');
});
it('Handles variable reassignment', function() {
const source = dedent`
[var]: one
$[var]
[var]: two
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>one</p><p>two</p>'.trimReturns());
});
it('Handles variable reassignment with hoisting', function() {
const source = dedent`
$[var]
[var]: one
$[var]
[var]: two
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>two</p><p>one</p><p>two</p>'.trimReturns());
});
it('Ignores undefined variables that can\'t be hoisted', function() {
const source = dedent`
$[var](My name is $[first] $[last])
$[last]: Jones
`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is $[first] Jones</p>`.trimReturns());
});
});
describe('Inline-level variables', ()=>{
it('Handles variable assignment and recall with simple text', function() {
const source = dedent`
$[var](string)
$[var]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>string</p><p>string</p>');
});
it('Hoists undefined variables when possible', function() {
const source = dedent`
$[var](My name is $[name] Jones)
[name]: Bob`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>My name is Bob Jones</p>');
});
it('Hoists last instance of variable', function() {
const source = dedent`
$[var](My name is $[name] Jones)
$[name](Bob)
[name]: Bill`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p>My name is Bill Jones</p> <p>Bob</p>`.trimReturns());
});
it('Only captures nested parens if balanced', function() {
const source = dedent`
$[var1](A variable (with nested parens) inside)
$[var1]
$[var2](A variable ) with unbalanced parens)
$[var2]`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>A variable (with nested parens) inside</p>
<p>A variable (with nested parens) inside</p>
<p>A variable with unbalanced parens)</p>
<p>A variable</p>
`.trimReturns());
});
});
describe('Math', ()=>{
it('Handles simple math using numbers only', function() {
const source = dedent`
$[1 + 3 * 5 - (1 / 4)]
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>15.75</p>');
});
it('Handles round function', function() {
const source = dedent`
$[round(1/4)]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
});
it('Handles floor function', function() {
const source = dedent`
$[floor(0.6)]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>0</p>');
});
it('Handles ceil function', function() {
const source = dedent`
$[ceil(0.2)]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
});
it('Handles nested functions', function() {
const source = dedent`
$[ceil(floor(round(0.6)))]`;
const rendered = Markdown.render(source).replace(/\s/g, ' ').trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>1</p>');
});
it('Handles simple math with variables', function() {
const source = dedent`
$[num1]: 5
$[num2]: 4
Answer is $[answer]($[1 + 3 * num1 - (1 / num2)]).
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Answer is 15.75.</p>');
});
it('Handles variable incrementing', function() {
const source = dedent`
$[num1]: 5
Increment num1 to get $[num1]($[num1 + 1]) and again to $[num1]($[num1 + 1]).
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('<p>Increment num1 to get 6 and again to 7.</p>');
});
});
describe('Code blocks', ()=>{
it('Ignores all variables in fenced code blocks', function() {
const source = dedent`
\`\`\`
[var]: string
$[var]
$[var](new string)
\`\`\`
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<pre><code>
[var]: string
$[var]
$[var](new string)
</code></pre>`.trimReturns());
});
it('Ignores all variables in indented code blocks', function() {
const source = dedent`
test
[var]: string
$[var]
$[var](new string)
`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>test</p>
<pre><code>
[var]: string
$[var]
$[var](new string)
</code></pre>`.trimReturns());
});
it('Ignores all variables in inline code blocks', function() {
const source = '[var](Hello) `[link](url)`. This `[var] does not work`';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><a href="Hello">var</a> <code>[link](url)</code>. This <code>[var] does not work</code></p>`.trimReturns());
});
});
describe('Normal Links and Images', ()=>{
it('Renders normal images', function() {
const source = `![alt text](url)`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img src="url" alt="alt text"></p>`.trimReturns());
});
it('Renders normal images with a title', function() {
const source = 'An image ![alt text](url "and title")!';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>An image <img src="url" alt="alt text" title="and title">!</p>`.trimReturns());
});
it('Applies curly injectors to images', function() {
const source = `![alt text](url){width:100px}`;
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p><img class="" style="width:100px;" src="url" alt="alt text"></p>`.trimReturns());
});
it('Renders normal links', function() {
const source = 'A Link to my [website](url)!';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>A Link to my <a href="url">website</a>!</p>`.trimReturns());
});
it('Renders normal links with a title', function() {
const source = 'A Link to my [website](url "and title")!';
const rendered = Markdown.render(source).trimReturns();
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(dedent`
<p>A Link to my <a href="url" title="and title">website</a>!</p>`.trimReturns());
});
});
describe('Cross-page variables', ()=>{
it('Handles variable assignment and recall across pages', function() {
const source0 = `[var]: string`;
const source1 = `$[var]`;
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('\\page<p>string</p>');
});
it('Handles hoisting across pages', function() {
const source0 = `$[var]`;
const source1 = `[var]: string`;
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>string</p>\\page');
});
it('Handles reassignment and hoisting across pages', function() {
const source0 = `$[var]\n\n[var]: one\n\n$[var]`;
const source1 = `[var]: two\n\n$[var]`;
renderAllPages([source0, source1]).join('\n\\page\n').trimReturns(); //Requires one full render of document before hoisting is picked up
const rendered = renderAllPages([source0, source1]).join('\n\\page\n').trimReturns();
expect(rendered, `Input:\n${[source0, source1].join('\n\\page\n')}`, { showPrefix: false }).toBe('<p>two</p><p>one</p>\\page<p>two</p>');
});
});

View File

@@ -149,6 +149,8 @@ module.exports = {
![](/assets/naturalCritLogoWhite.svg)
Homebrewery.Naturalcrit.com
}}`;
}}
\page`;
}
};

View File

@@ -78,7 +78,7 @@ module.exports = function(props){
return dedent`
{{toc,wide
# Contents
# Table Of Contents
${markdown}
}}

View File

@@ -1,6 +1,6 @@
@import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less';
@import (less) './themes/fonts/icon fonts/font-icons.less';
@import (less) './themes/fonts/icon fonts/diceFont.less';
:root {
//Colors
@@ -307,6 +307,7 @@
margin-left : -0.16cm;
background-color : var(--HB_Color_MonsterStatBackground);
background-image : @monsterBlockBackground;
background-attachment : fixed;
background-blend-mode : overlay;
border-style : solid;
border-width : 7px 6px;
@@ -402,9 +403,15 @@
}
}
.pageNumber {
position : absolute;
right : 2px;
bottom : 22px;
width : 50px;
font-size : 0.9em;
color : var(--HB_Color_Footnotes);
text-align : center;
text-indent : 0;
&.auto::after { content : counter(phb-page-numbers); }
}
.footnote {
position : absolute;
@@ -532,14 +539,14 @@
.page:has(.frontCover) {
columns : 1;
text-align : center;
&::after { display : none; }
&::after { all : unset; }
h1 {
margin-top : 1.2cm;
margin-bottom : 0;
font-family : 'NodestoCapsCondensed';
font-size : 2.245cm;
font-weight : normal;
line-height : 1.9cm;
line-height : 0.85em;
color : white;
text-shadow : unset;
text-transform : uppercase;
@@ -626,14 +633,14 @@
.page:has(.insideCover) {
columns : 1;
text-align : center;
&::after { display : none; }
&::after { all : unset; }
h1 {
margin-top : 1.2cm;
margin-bottom : 0;
font-family : 'NodestoCapsCondensed';
font-size : 2.1cm;
font-weight : normal;
line-height : 1.785cm;
line-height : 0.85em;
text-transform : uppercase;
}
h2 {
@@ -672,7 +679,7 @@
padding : 2.25cm 1.3cm 2cm 1.3cm;
color : #FFFFFF;
columns : 1;
&::after { display : none; }
&::after { all : unset; }
.columnWrapper { width : 7.6cm; }
.backCover {
position : absolute;
@@ -688,7 +695,7 @@
margin-bottom : 0.3cm;
font-family : 'NodestoCapsCondensed';
font-size : 1.35cm;
line-height : 1.28cm;
line-height : 0.95em;
color : #ED1C24;
text-align : center;
}
@@ -714,7 +721,7 @@
p {
font-family : 'Overpass';
font-size : 0.332cm;
line-height : 0.35cm;
line-height : 1.5em;
}
hr + p {
margin-top : 0.6cm;
@@ -739,10 +746,10 @@
font-family : 'NodestoCapsWide';
font-size : 0.4cm;
line-height : 1em;
line-height : 1.28cm;
color : #FFFFFF;
text-align : center;
text-indent : 0;
letter-spacing : 0.08em;
}
}
}
@@ -782,7 +789,7 @@
margin-left : auto;
font-family : 'Overpass';
font-size : 0.45cm;
line-height : 0.495cm;
line-height : 1.1em;
}
}

View File

@@ -326,27 +326,27 @@ module.exports = [
gen : dedent`{{font-family:CodeLight Dummy Text}}`
},
{
name : 'Scaly Sans',
name : 'Scaly Sans Remake',
icon : 'font ScalySansRemake',
gen : dedent`{{font-family:ScalySansRemake Dummy Text}}`
},
{
name : 'Book Insanity',
name : 'Book Insanity Remake',
icon : 'font BookInsanityRemake',
gen : dedent`{{font-family:BookInsanityRemake Dummy Text}}`
},
{
name : 'Mr Eaves',
name : 'Mr Eaves Remake',
icon : 'font MrEavesRemake',
gen : dedent`{{font-family:MrEavesRemake Dummy Text}}`
},
{
name: 'Solbera Imitation',
name: 'Solbera Imitation Remake',
icon: 'font SolberaImitationRemake',
gen: dedent`{{font-family:SolberaImitationRemake Dummy Text}}`
},
{
name: 'Scaly Sans Small Caps',
name: 'Scaly Sans Small Caps Remake',
icon: 'font ScalySansSmallCapsRemake',
gen: dedent`{{font-family:ScalySansSmallCapsRemake Dummy Text}}`
},

View File

@@ -1,6 +1,5 @@
@import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less';
@import (less) './themes/fonts/icon fonts/diceFont.less';
:root {
//Colors
@@ -9,7 +8,7 @@
}
@page { margin : 0; }
body { counter-reset : page-numbers; }
body { counter-reset : phb-page-numbers; }
* { -webkit-print-color-adjust : exact; }
//*****************************
@@ -48,7 +47,7 @@ body { counter-reset : page-numbers; }
height : 279.4mm;
padding : 1.4cm 1.9cm 1.7cm;
overflow : hidden;
counter-increment : page-numbers;
counter-increment : phb-page-numbers;
background-color : var(--HB_Color_Background);
text-rendering : optimizeLegibility;
contain : size;
@@ -167,6 +166,7 @@ body { counter-reset : page-numbers; }
margin : 0;
font-size : 120px;
text-transform : uppercase;
mix-blend-mode : overlay;
opacity : 30%;
transform : rotate(-45deg);
p { margin-bottom : none; }
@@ -460,22 +460,3 @@ body { counter-reset : page-numbers; }
.homebreweryIcon.red { background-color : red; }
.homebreweryIcon.gold { background-image : linear-gradient(to top left, brown 22.5%, gold 40%, white 60%, gold 67.5%, brown 82.5%); }
}
//*****************************
//* Page Number
//*****************************/
.page {
.pageNumber {
position : absolute;
right : 30px;
bottom : 30px;
width : 50px;
font-size : 0.9em;
text-align : center;
&.auto::after { content : counter(page-numbers); }
}
&:nth-child(even) {
.pageNumber { left : 30px; }
}
}

View File

@@ -374,9 +374,17 @@
}
.pageNumber{
font-family : FrederickaTheGreat;
position : absolute;
right : 3cm;
bottom : 1.25cm;
width : 50px;
font-size : 0.9em;
color : var(--HB_Color_HeaderText);
text-align : center;
text-indent : 0;
&.auto::after {
content : counter(phb-page-numbers);
}
}
.footnote{
position : absolute;

View File

@@ -1,121 +0,0 @@
.CodeMirror {
background: #0C0C0C;
color: #B9BDB6;
}
/* Brew BG */
.brewRenderer {
background-color: #0C0C0C;
}
.cm-s-darkvision {
/* Blinking cursor and selection */
.CodeMirror-cursor {
border-left: 1px solid #B9BDB6;
}
.CodeMirror-selected {
background: #E0E8FF40;
}
/* Line number stuff */
.CodeMirror-gutter-elt {
color: #81969A;
}
.CodeMirror-linenumber {
background-color: #0C0C0C;
}
.CodeMirror-gutter {
background-color: #0C0C0C;
}
/* column splits */
.editor .codeEditor .columnSplit {
font-style: italic;
color: inherit;
background-color:#1F5763;
border-bottom: #299 solid 1px;
}
/* # headings */
.cm-header {
color: #C51B1B;
-webkit-text-stroke-width: 0.1px;
}
/* bold points */
.cm-strong {
font-weight: bold;
color: #309DD2;
}
/* Link headings */
.cm-link {
color: #DD6300;
}
/* links */
.cm-string {
color: #5CE638;
}
/*@import*/
.cm-def {
color: #2986CC;
}
/* Bullets and such */
.cm-variable-2 {
color: #3CBF30;
}
/* Tags (divs) */
.cm-tag {
color: #E3FF00;
}
.cm-attribute {
color: #E3FF00;
}
.cm-atom {
color: #CF7EA9;
}
.cm-qualifier {
color: #EE1919;
}
.cm-comment {
color: #BBC700;
}
.cm-keyword {
color: #CC66FF;
}
.cm-property {
color: aqua;
}
.cm-error {
color: #C50202;
}
.CodeMirror-foldmarker {
color: #F0FF00;
}
/* New page */
.cm-builtin {
color: #FFF;
}
}
.editor .codeEditor {
/* blocks */
.block:not(.cm-comment) {
color: magenta;
}
/* definition lists */
.define.definition {
color: #FFAA3E;
}
.define.term {
color: #7290d9;
}
.define:not(.term):not(.definition) {
background: #333;
}
/* New page */
.pageLine {
background: #000;
color: #000;
border-bottom: 1px solid #FFF;
}
}

View File

@@ -16,7 +16,6 @@
"colorforth",
"darcula",
"darkbrewery-v301",
"darkvision",
"dracula",
"duotone-dark",
"duotone-light",

View File

@@ -1,114 +0,0 @@
/* Icon Font: diceFont */
@font-face {
font-family : 'DiceFont';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/icon fonts/diceFont.woff2');
}
.df {
display : inline-block;
font-family : 'DiceFont';
font-style : normal;
font-weight : normal;
font-variant : normal;
line-height : 1;
text-decoration : inherit;
text-transform : none;
text-rendering : optimizeLegibility;
-moz-osx-font-smoothing : grayscale;
-webkit-font-smoothing : antialiased;
&.F::before { content : '\f190'; }
&.F-minus::before { content : '\f191'; }
&.F-plus::before { content : '\f192'; }
&.F-zero::before { content : '\f193'; }
&.d10::before { content : '\f194'; }
&.d10-0::before { content : '\f100'; }
&.d10-1::before { content : '\f101'; }
&.d10-10::before { content : '\f102'; }
&.d10-2::before { content : '\f103'; }
&.d10-3::before { content : '\f104'; }
&.d10-4::before { content : '\f105'; }
&.d10-5::before { content : '\f106'; }
&.d10-6::before { content : '\f107'; }
&.d10-7::before { content : '\f108'; }
&.d10-8::before { content : '\f109'; }
&.d10-9::before { content : '\f10a'; }
&.d12::before { content : '\f195'; }
&.d12-1::before { content : '\f10b'; }
&.d12-10::before { content : '\f10c'; }
&.d12-11::before { content : '\f10d'; }
&.d12-12::before { content : '\f10e'; }
&.d12-2::before { content : '\f10f'; }
&.d12-3::before { content : '\f110'; }
&.d12-4::before { content : '\f111'; }
&.d12-5::before { content : '\f112'; }
&.d12-6::before { content : '\f113'; }
&.d12-7::before { content : '\f114'; }
&.d12-8::before { content : '\f115'; }
&.d12-9::before { content : '\f116'; }
&.d2::before { content : '\f196'; }
&.d2-1::before { content : '\f117'; }
&.d2-2::before { content : '\f118'; }
&.d20::before { content : '\f197'; }
&.d20-1::before { content : '\f119'; }
&.d20-10::before { content : '\f11a'; }
&.d20-11::before { content : '\f11b'; }
&.d20-12::before { content : '\f11c'; }
&.d20-13::before { content : '\f11d'; }
&.d20-14::before { content : '\f11e'; }
&.d20-15::before { content : '\f11f'; }
&.d20-16::before { content : '\f120'; }
&.d20-17::before { content : '\f121'; }
&.d20-18::before { content : '\f122'; }
&.d20-19::before { content : '\f123'; }
&.d20-2::before { content : '\f124'; }
&.d20-20::before { content : '\f125'; }
&.d20-3::before { content : '\f126'; }
&.d20-4::before { content : '\f127'; }
&.d20-5::before { content : '\f128'; }
&.d20-6::before { content : '\f129'; }
&.d20-7::before { content : '\f12a'; }
&.d20-8::before { content : '\f12b'; }
&.d20-9::before { content : '\f12c'; }
&.d4::before { content : '\f198'; }
&.d4-1::before { content : '\f12d'; }
&.d4-2::before { content : '\f12e'; }
&.d4-3::before { content : '\f12f'; }
&.d4-4::before { content : '\f130'; }
&.d6::before { content : '\f199'; }
&.d6-1::before { content : '\f131'; }
&.d6-2::before { content : '\f132'; }
&.d6-3::before { content : '\f133'; }
&.d6-4::before { content : '\f134'; }
&.d6-5::before { content : '\f135'; }
&.d6-6::before { content : '\f136'; }
&.d8::before { content : '\f19a'; }
&.d8-1::before { content : '\f137'; }
&.d8-2::before { content : '\f138'; }
&.d8-3::before { content : '\f139'; }
&.d8-4::before { content : '\f13a'; }
&.d8-5::before { content : '\f13b'; }
&.d8-6::before { content : '\f13c'; }
&.d8-7::before { content : '\f13d'; }
&.d8-8::before { content : '\f13e'; }
&.dot-d6::before { content : '\f19b'; }
&.dot-d6-1::before { content : '\f13f'; }
&.dot-d6-2::before { content : '\f140'; }
&.dot-d6-3::before { content : '\f141'; }
&.dot-d6-4::before { content : '\f142'; }
&.dot-d6-5::before { content : '\f143'; }
&.dot-d6-6::before { content : '\f18f'; }
&.small-dot-d6-1::before { content : '\f183'; }
&.small-dot-d6-2::before { content : '\f184'; }
&.small-dot-d6-3::before { content : '\f185'; }
&.small-dot-d6-4::before { content : '\f186'; }
&.small-dot-d6-5::before { content : '\f187'; }
&.small-dot-d6-6::before { content : '\f188'; }
&.solid-small-dot-d6-1::before { content : '\f189'; }
&.solid-small-dot-d6-2::before { content : '\f18a'; }
&.solid-small-dot-d6-3::before { content : '\f18b'; }
&.solid-small-dot-d6-4::before { content : '\f18c'; }
&.solid-small-dot-d6-5::before { content : '\f18d'; }
&.solid-small-dot-d6-6::before { content : '\f18e'; }
}

View File

@@ -1,18 +0,0 @@
# License
DiceFont is open source. You can use it for commercial projects, personal
projects or open source projects.
## Font License
Applies to all desktop and webfont files: [License: SIL OFL 1.1](http://scripts.sil.org/OFL)
## Code License
Applies to all CSS and LESS files: [License: MIT License](http://opensource.org/licenses/mit-license.html)
## Documentation License
Applies to all other files [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/)
Copyright [Franco Ponticelli](https://github.com/fponticelli).

View File

@@ -1,6 +1,6 @@
/* Icon Font: Elderberry Inn */
/* Main Font, serif */
@font-face {
font-family : 'Elderberry-Inn';
font-family : 'Eldeberry-Inn';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/icon fonts/Elderberry-Inn-Icons.woff2');
@@ -10,7 +10,7 @@
span.ei {
display : inline-block;
margin-right : 3px;
font-family : 'Elderberry-Inn';
font-family : 'Eldeberry-Inn';
line-height : 1;
vertical-align : baseline;
-moz-osx-font-smoothing : grayscale;