diff --git a/.circleci/config.yml b/.circleci/config.yml index 2025e8fe7..d02402927 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ orbs: jobs: build: docker: - - image: cimg/node:20.17.0 + - image: cimg/node:20.18.0 - image: mongo:4.4 working_directory: ~/homebrewery diff --git a/README.md b/README.md index df7f41503..5206f4cbf 100644 --- a/README.md +++ b/README.md @@ -144,3 +144,4 @@ your contribution to the project, please join our [gitter chat][gitter-url]. [github-mark-duplicate-url]: https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/about-duplicate-issues-and-pull-requests [github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request [gitter-url]: https://gitter.im/naturalcrit/Lobby + diff --git a/client/admin/admin.jsx b/client/admin/admin.jsx index 95c296259..29973d221 100644 --- a/client/admin/admin.jsx +++ b/client/admin/admin.jsx @@ -3,14 +3,15 @@ import React, { useEffect, useState } from 'react'; const BrewUtils = require('./brewUtils/brewUtils.jsx'); const NotificationUtils = require('./notificationUtils/notificationUtils.jsx'); import AuthorUtils from './authorUtils/authorUtils.jsx'; +import LockTools from './lockTools/lockTools.jsx'; -const tabGroups = ['brew', 'notifications', 'authors']; +const tabGroups = ['brew', 'notifications', 'authors', 'locks']; const Admin = ()=>{ - const [currentTab, setCurrentTab] = useState('brew'); + const [currentTab, setCurrentTab] = useState(''); useEffect(()=>{ - setCurrentTab(localStorage.getItem('hbAdminTab')); + setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew'); }, []); useEffect(()=>{ @@ -40,6 +41,7 @@ const Admin = ()=>{ {currentTab === 'brew' && } {currentTab === 'notifications' && } {currentTab === 'authors' && } + {currentTab === 'locks' && } ); diff --git a/client/admin/lockTools/lockTools.jsx b/client/admin/lockTools/lockTools.jsx new file mode 100644 index 000000000..9a28d330f --- /dev/null +++ b/client/admin/lockTools/lockTools.jsx @@ -0,0 +1,342 @@ +/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ +require('./lockTools.less'); +const React = require('react'); +const createClass = require('create-react-class'); + +import request from '../../homebrew/utils/request-middleware.js'; + +const LockTools = createClass({ + displayName : 'LockTools', + getInitialState : function() { + return { + fetching : false, + reviewCount : 0 + }; + }, + + componentDidMount : function() { + this.updateReviewCount(); + }, + + updateReviewCount : async function() { + const newCount = await request.get('/api/lock/count') + .then((res)=>{return res.body?.count || 'Unknown';}); + if(newCount != this.state.reviewCount){ + this.setState({ + reviewCount : newCount + }); + } + }, + + updateLockData : function(lock){ + this.setState({ + lock : lock + }); + }, + + render : function() { + return
+

Lock Count

+

Number of brews currently locked: {this.state.reviewCount}

+ +
+ +
+ +
+ +
+
+ + +
+
+
; + } +}); + +const LockBrew = createClass({ + displayName : 'LockBrew', + getInitialState : function() { + // Default values + return { + brewId : this.props.lock?.shareId || '', + code : this.props.lock?.code || 455, + editMessage : this.props.lock?.editMessage || '', + shareMessage : this.props.lock?.shareMessage || 'This Brew has been locked.', + result : {}, + overwrite : false, + }; + }, + + handleChange : function(e, varName) { + const output = {}; + output[varName] = e.target.value; + this.setState(output); + }, + + submit : function(e){ + e.preventDefault(); + if(!this.state.editMessage) return; + const newLock = { + overwrite : this.state.overwrite, + code : parseInt(this.state.code) || 100, + editMessage : this.state.editMessage, + shareMessage : this.state.shareMessage, + applied : new Date + }; + + request.post(`/api/lock/${this.state.brewId}`) + .send(newLock) + .set('Content-Type', 'application/json') + .then((response)=>{ + this.setState({ result: response.body }); + }) + .catch((err)=>{ + this.setState({ result: err.response.body }); + }); + }, + + renderInput : function (name) { + return this.handleChange(e, name)} autoComplete='off' required/>; + }, + + renderResult : function(){ + return <> +

Result:

+ + + {Object.keys(this.state.result).map((key, idx)=>{ + return + + + ; + })} + +
{key}{this.state.result[key].toString()} +
+ ; + }, + + render : function() { + return
+
+

Lock Brew

+
+ +
+ +
+ +
+ +
+ + +
+ {this.state.result && this.renderResult()} +
+
+

Suggestions

+
+

Codes

+
    +
  • 455 - Generic Lock
  • +
  • 456 - Copyright issues
  • +
  • 457 - Confidential Information Leakage
  • +
  • 458 - Sensitive Personal Information
  • +
  • 459 - Defamation or Libel
  • +
  • 460 - Hate Speech or Discrimination
  • +
  • 461 - Illegal Activities
  • +
  • 462 - Malware or Phishing
  • +
  • 463 - Plagiarism
  • +
  • 465 - Misrepresentation
  • +
  • 466 - Inappropriate Content
  • +
+
+
+

Messages

+
    +
  • Private Message: This is the private message that is ONLY displayed to the authors of the locked brew. This message MUST specify exactly what actions must be taken in order to have the brew unlocked.
  • +
  • Public Message: This is the public message that is displayed to the EVERYONE that attempts to view the locked brew.
  • +
+
+
+
; + } +}); + +const LockTable = createClass({ + displayName : 'LockTable', + getDefaultProps : function() { + return { + title : '', + text : '', + fetchURL : '/api/locks', + resultName : '', + propertyNames : ['shareId'], + loadBrew : ()=>{} + }; + }, + + getInitialState : function() { + return { + result : '', + error : '', + searching : false + }; + }, + + lockKey : React.createRef(0), + + clickFn : function (){ + this.setState({ searching: true, error: null }); + + request.get(this.props.fetchURL) + .then((res)=>this.setState({ result: res.body })) + .catch((err)=>this.setState({ result: err.response.body })) + .finally(()=>{ + this.setState({ searching: false }); + }); + }, + + updateBrewLockData : function (lockData){ + this.lockKey.current++; + const brewData = { + key : this.lockKey.current, + shareId : lockData.shareId, + code : lockData.lock.code, + editMessage : lockData.lock.editMessage, + shareMessage : lockData.lock.shareMessage + }; + this.props.loadBrew(brewData); + }, + + render : function () { + return <> +
+
+

{this.props.title}

+ +
+ {this.state.result[this.props.resultName] && + <> +

{this.props.text}: {this.state.result[this.props.resultName].length}

+ + + + {this.props.propertyNames.map((name, idx)=>{ + return ; + })} + + + + + + {this.state.result[this.props.resultName].map((result, resultIdx)=>{ + return + {this.props.propertyNames.map((name, nameIdx)=>{ + return ; + })} + + + ; + })} + +
{name}clipload
+ {result[name].toString()} + {navigator.clipboard.writeText(result.shareId.toString());}}>{this.updateBrewLockData(result);}}>
+ + } +
+ ; + } +}); + +const LockLookup = createClass({ + displayName : 'LockLookup', + getDefaultProps : function() { + return { + fetchURL : '/api/lookup' + }; + }, + + getInitialState : function() { + return { + query : '', + result : '', + error : '', + searching : false + }; + }, + + handleChange(e){ + this.setState({ query: e.target.value }); + }, + + clickFn(){ + this.setState({ searching: true, error: null }); + + request.put(`${this.props.fetchURL}/${this.state.query}`) + .then((res)=>this.setState({ result: res.body })) + .catch((err)=>this.setState({ result: err.response.body })) + .finally(()=>{ + this.setState({ searching: false }); + }); + }, + + renderResult : function(){ + return
+

Result:

+ + + {Object.keys(this.state.result).map((key, idx)=>{ + return + + + ; + })} + +
{key}{this.state.result[key].toString()} +
+
; + }, + + render : function() { + return
+

{this.props.title}

+ + + + {this.state.error + &&
{this.state.error.toString()}
+ } + + {this.state.result && this.renderResult()} +
; + } +}); + +module.exports = LockTools; \ No newline at end of file diff --git a/client/admin/lockTools/lockTools.less b/client/admin/lockTools/lockTools.less new file mode 100644 index 000000000..1ec9c524a --- /dev/null +++ b/client/admin/lockTools/lockTools.less @@ -0,0 +1,66 @@ +.lockTools { + .lockBrew { + columns : 2; + + .lockForm { + break-inside : avoid; + + label { + display : inline-block; + width : 100%; + line-height : 2.25em; + text-align : right; + input { + float : right; + width : 65%; + margin-left : 10px; + } + &.checkbox { + line-height: 1.5em; + input { + width : 1.5em; + height : 1.5em; + } + } + } + } + + .lockSuggestions { + line-height : 1.2em; + break-inside : avoid; + columns : 2; + h2 { column-span : all; } + h3 { margin-top : 0px; } + b { font-weight : 600; } + + .lockCodes { break-inside : avoid; } + } + } + + .lockTable { + cursor : default; + break-inside : avoid; + .row:hover { + color : #000000; + background-color : #CCCCCC; + } + .icon { + cursor : pointer; + &:hover { text-shadow : 0px 0px 6px black; } + } + } + + th, td { + padding : 4px 10px; + text-align : center; + } + table, td { border : 1px solid #333333; } + + .brewLookup { + min-height : 175px; + break-inside : avoid; + h2 { margin-top : 0px; } + } + + button i { padding-left : 5px; } +} \ No newline at end of file diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index a58980377..6859c5aa2 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -13,6 +13,8 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?: *{[^\n{}]*})?$)/m; +const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; +const SNIPPETBAR_HEIGHT = 25; const DEFAULT_STYLE_TEXT = dedent` /*=======--- Example CSS styling ---=======*/ /* Any CSS here will apply to your document! */ @@ -21,6 +23,13 @@ const DEFAULT_STYLE_TEXT = dedent` color: black; }`; +const DEFAULT_SNIPPET_TEXT = dedent` + \snippet example snippet + + The text between \`\snippet title\` lines will become a snippet of name \`title\` as this example provides. + + This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme. +`; let isJumping = false; const Editor = createClass({ @@ -35,6 +44,7 @@ const Editor = createClass({ onTextChange : ()=>{}, onStyleChange : ()=>{}, onMetaChange : ()=>{}, + onSnipChange : ()=>{}, reportError : ()=>{}, onCursorPageChange : ()=>{}, @@ -51,7 +61,7 @@ const Editor = createClass({ getInitialState : function() { return { editorTheme : this.props.editorTheme, - view : 'text' //'text', 'style', 'meta' + view : 'text' //'text', 'style', 'meta', 'snippet' }; }, @@ -61,6 +71,7 @@ const Editor = createClass({ isText : function() {return this.state.view == 'text';}, isStyle : function() {return this.state.view == 'style';}, isMeta : function() {return this.state.view == 'meta';}, + isSnip : function() {return this.state.view == 'snippet';}, componentDidMount : function() { @@ -131,6 +142,7 @@ const Editor = createClass({ handleViewChange : function(newView){ this.props.setMoveArrows(newView === 'text'); + this.setState({ view : newView }, ()=>{ @@ -140,7 +152,7 @@ const Editor = createClass({ highlightCustomMarkdown : function(){ if(!this.codeEditor.current) return; - if(this.state.view === 'text') { + if((this.state.view === 'text') ||(this.state.view === 'snippet')) { const codeMirror = this.codeEditor.current.codeMirror; codeMirror.operation(()=>{ // Batch CodeMirror styling @@ -159,12 +171,18 @@ const Editor = createClass({ for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear(); + let userSnippetCount = 1; // start snippet count from snippet 1 let editorPageCount = 1; // start page count from page 1 - _.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{ + const whichSource = this.state.view === 'text' ? this.props.brew.text : this.props.brew.snippets; + _.forEach(whichSource?.split('\n'), (line, lineNumber)=>{ + + const tabHighlight = this.state.view === 'text' ? 'pageLine' : 'snippetLine'; + const textOrSnip = this.state.view === 'text'; //reset custom line styles codeMirror.removeLineClass(lineNumber, 'background', 'pageLine'); + codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine'); codeMirror.removeLineClass(lineNumber, 'text'); codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash'); @@ -175,22 +193,24 @@ const Editor = createClass({ // Styling for \page breaks if((this.props.renderer == 'legacy' && line.includes('\\page')) || - (this.props.renderer == 'V3' && line.match(PAGEBREAK_REGEX_V3))) { + (this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) { - if(lineNumber > 0) // Since \page is optional on first line of document, + if((lineNumber > 0) && (textOrSnip)) // Since \page is optional on first line of document, editorPageCount += 1; // don't use it to increment page count; stay at 1 + else if(this.state.view !== 'text') userSnippetCount += 1; // add back the original class 'background' but also add the new class '.pageline' - codeMirror.addLineClass(lineNumber, 'background', 'pageLine'); + codeMirror.addLineClass(lineNumber, 'background', tabHighlight); const pageCountElement = Object.assign(document.createElement('span'), { className : 'editor-page-count', - textContent : editorPageCount + textContent : textOrSnip ? editorPageCount : userSnippetCount }); codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement); }; + // New Codemirror styling for V3 renderer - if(this.props.renderer == 'V3') { + if(this.props.renderer === 'V3') { if(line.match(/^\\column$/)){ codeMirror.addLineClass(lineNumber, 'text', 'columnSplit'); } @@ -446,6 +466,21 @@ const Editor = createClass({ userThemes={this.props.userThemes}/> ; } + + if(this.isSnip()){ + if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; } + return <> + + ; + } }, redo : function(){ @@ -486,7 +521,7 @@ const Editor = createClass({ historySize={this.historySize()} currentEditorTheme={this.state.editorTheme} updateEditorTheme={this.updateEditorTheme} - snippetBundle={this.props.snippetBundle} + themeBundle={this.props.themeBundle} cursorPos={this.codeEditor.current?.getCursorPosition() || {}} updateBrew={this.props.updateBrew} /> diff --git a/client/homebrew/editor/editor.less b/client/homebrew/editor/editor.less index 79859f5db..7fbed0ff7 100644 --- a/client/homebrew/editor/editor.less +++ b/client/homebrew/editor/editor.less @@ -7,7 +7,7 @@ .codeEditor { height : 100%; .CodeMirror { height : 100%; } - .pageLine { + .pageLine, .snippetLine { background : #33333328; border-top : #333399 solid 1px; } @@ -15,6 +15,10 @@ float : right; color : grey; } + .editor-snippet-count { + float : right; + color : grey; + } .columnSplit { font-style : italic; color : grey; diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index 4d702bfdf..5e2051a86 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -6,6 +6,7 @@ const _ = require('lodash'); const cx = require('classnames'); import { loadHistory } from '../../utils/versionHistory.js'; +import { brewSnippetsToJSON } from '../../../../shared/helpers.js'; //Import all themes const ThemeSnippets = {}; @@ -40,7 +41,7 @@ const Snippetbar = createClass({ unfoldCode : ()=>{}, updateEditorTheme : ()=>{}, cursorPos : {}, - snippetBundle : [], + themeBundle : [], updateBrew : ()=>{} }; }, @@ -64,7 +65,10 @@ const Snippetbar = createClass({ }, componentDidUpdate : async function(prevProps, prevState) { - if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) { + if(prevProps.renderer != this.props.renderer || + prevProps.theme != this.props.theme || + prevProps.themeBundle != this.props.themeBundle || + prevProps.brew.snippets != this.props.brew.snippets) { this.setState({ snippets : this.compileSnippets() }); @@ -97,7 +101,7 @@ const Snippetbar = createClass({ if(key == 'snippets') { const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme return result.filter((snip)=>snip.gen || snip.subsnippets); - } + }; }, compileSnippets : function() { @@ -105,15 +109,21 @@ const Snippetbar = createClass({ let oldSnippets = _.keyBy(compiledSnippets, 'groupName'); - for (let snippets of this.props.snippetBundle) { - if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name - snippets = ThemeSnippets[snippets]; + if(this.props.themeBundle.snippets) { + for (let snippets of this.props.themeBundle.snippets) { + if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name + snippets = ThemeSnippets[snippets]; - const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName'); - compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer)); + const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName'); + compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer)); - oldSnippets = _.keyBy(compiledSnippets, 'groupName'); + oldSnippets = _.keyBy(compiledSnippets, 'groupName'); + } } + + const userSnippetsasJSON = brewSnippetsToJSON(this.props.brew.title || 'New Document', this.props.brew.snippets, this.props.themeBundle.snippets); + compiledSnippets.push(userSnippetsasJSON); + return compiledSnippets; }, @@ -207,8 +217,6 @@ const Snippetbar = createClass({ renderEditorButtons : function(){ if(!this.props.showEditButtons) return; - - return (
{this.props.view !== 'meta' && <>
@@ -242,7 +250,6 @@ const Snippetbar = createClass({
} -
this.props.onViewChange('text')}> @@ -252,6 +259,10 @@ const Snippetbar = createClass({ onClick={()=>this.props.onViewChange('style')}>
+
this.props.onViewChange('snippet')}> + +
this.props.onViewChange('meta')}> @@ -272,11 +283,6 @@ const Snippetbar = createClass({ module.exports = Snippetbar; - - - - - const SnippetGroup = createClass({ displayName : 'SnippetGroup', getDefaultProps : function() { @@ -310,7 +316,8 @@ const SnippetGroup = createClass({ }, render : function(){ - return
+ const snippetGroup = `snippetGroup snippetBarButton ${this.props.snippets.length === 0 ? 'disabledSnippets' : ''}`; + return
{this.props.groupName} diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less index 0860a44bf..a0691f8b6 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.less +++ b/client/homebrew/editor/snippetbar/snippetbar.less @@ -14,13 +14,13 @@ .snippets { display : flex; justify-content : flex-start; - min-width : 327.58px; + min-width : 432.18px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied } .editors { display : flex; justify-content : flex-end; - min-width : 225px; + min-width : 250px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied &:only-child {min-width : unset; margin-left : auto;} @@ -51,6 +51,9 @@ &.meta { .tooltipLeft('Properties'); } + &.snip { + .tooltipLeft('Snippets'); + } &.undo { .tooltipLeft('Undo'); font-size : 0.75em; @@ -226,8 +229,15 @@ } } } + .disabledSnippets { + color: grey; + cursor: not-allowed; + + &:hover { background-color: #DDDDDD;} + } + } -@container editor (width < 553px) { +@container editor (width < 683px) { .snippetBar { .editors { flex : 1; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index dc2520373..f2b1e809f 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -150,6 +150,18 @@ const EditPage = createClass({ }), ()=>{if(this.state.autoSave) this.trySave();}); }, + handleSnipChange : function(snippet){ + //If there are errors, run the validator on every change to give quick feedback + let htmlErrors = this.state.htmlErrors; + if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + + this.setState((prevState)=>({ + brew : { ...prevState.brew, snippets: snippet }, + isPending : true, + htmlErrors : htmlErrors, + }), ()=>{if(this.state.autoSave) this.trySave();}); + }, + handleStyleChange : function(style){ this.setState((prevState)=>({ brew : { ...prevState.brew, style: style } @@ -443,7 +455,7 @@ const EditPage = createClass({ {this.renderNavbar()} - {this.props.brew.lock && } + {this.props.brew.lock && }
{}, - message : '', + shareId : 0, + disableLock : ()=>{}, + lock : {}, + message : 'Unable to retrieve Lock Message', + reviewRequested : false, ...props }; - const removeLock = ()=>{ - alert(`Not yet implemented - ID ${props.shareId}`); + const [reviewState, setReviewState] = React.useState(props.reviewRequested); + + const removeLock = async ()=>{ + await request.put(`/api/lock/review/request/${props.shareId}`) + .then(()=>{ + setReviewState(true); + }); + }; + + const renderReviewButton = function(){ + if(reviewState){ return ; }; + return ; }; return @@ -19,11 +32,11 @@ function LockNotification(props) {

This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.


LOCK REASON

-

{props.message || 'Unable to retrieve Lock Message'}

+

{props.message}


Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.

Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.

- + {renderReviewButton()}
; }; diff --git a/client/homebrew/pages/editPage/lockNotification/lockNotification.less b/client/homebrew/pages/editPage/lockNotification/lockNotification.less index 54f1a9569..930b070c4 100644 --- a/client/homebrew/pages/editPage/lockNotification/lockNotification.less +++ b/client/homebrew/pages/editPage/lockNotification/lockNotification.less @@ -11,10 +11,12 @@ &::backdrop { background-color : #000000AA; } button { + padding : 2px 15px; margin : 10px; color : white; background-color : #333333; + &.inactive, &:hover { background-color : #777777; } } diff --git a/client/homebrew/pages/errorPage/errors/errorIndex.js b/client/homebrew/pages/errorPage/errors/errorIndex.js index 9584a14b9..b13b19eb1 100644 --- a/client/homebrew/pages/errorPage/errors/errorIndex.js +++ b/client/homebrew/pages/errorPage/errors/errorIndex.js @@ -194,13 +194,47 @@ const errorIndex = (props)=>{ **Brew ID:** ${props.brew.brewId} - **Brew Title:** ${escape(props.brew.brewTitle)}`, + **Brew Title:** ${escape(props.brew.brewTitle)} + + **Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`, // ####### Admin page error ####### '52' : dedent` ## Access Denied You need to provide correct administrator credentials to access this page.`, + // ####### Lock Errors + + '60' : dedent`Lock Error: General`, + + '61' : dedent`Lock Get Error: Unable to get lock count`, + + '62' : dedent`Lock Set Error: Cannot lock`, + + '63' : dedent`Lock Set Error: Brew not found`, + + '64' : dedent`Lock Set Error: Already locked`, + + '65' : dedent`Lock Remove Error: Cannot unlock`, + + '66' : dedent`Lock Remove Error: Brew not found`, + + '67' : dedent`Lock Remove Error: Not locked`, + + '68' : dedent`Lock Get Review Error: Cannot get review requests`, + + '69' : dedent`Lock Set Review Error: Cannot set review request`, + + '70' : dedent`Lock Set Review Error: Brew not found`, + + '71' : dedent`Lock Set Review Error: Review already requested`, + + '72' : dedent`Lock Remove Review Error: Cannot clear review request`, + + '73' : dedent`Lock Remove Review Error: Brew not found`, + + // ####### Other Errors + '90' : dedent` An unexpected error occurred while looking for these brews. Try again in a few minutes.`, diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index b9c1b7371..d03e30c91 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -108,7 +108,7 @@ const HomePage = createClass({ onTextChange={this.handleTextChange} renderer={this.state.brew.renderer} showEditButtons={false} - snippetBundle={this.state.themeBundle.snippets} + themeBundle={this.state.themeBundle} onCursorPageChange={this.handleEditorCursorPageChange} onViewPageChange={this.handleEditorViewPageChange} currentEditorViewPageNum={this.state.currentEditorViewPageNum} diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 7aaeb3fd3..3e4e5ce41 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -141,6 +141,18 @@ const NewPage = createClass({ localStorage.setItem(STYLEKEY, style); }, + handleSnipChange : function(snippet){ + //If there are errors, run the validator on every change to give quick feedback + let htmlErrors = this.state.htmlErrors; + if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + + this.setState((prevState)=>({ + brew : { ...prevState.brew, snippets: snippet }, + isPending : true, + htmlErrors : htmlErrors, + }), ()=>{if(this.state.autoSave) this.trySave();}); + }, + handleMetaChange : function(metadata, field=undefined){ if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed fetchThemeBundle(this, metadata.renderer, metadata.theme); @@ -231,10 +243,10 @@ const NewPage = createClass({ onTextChange={this.handleTextChange} onStyleChange={this.handleStyleChange} onMetaChange={this.handleMetaChange} + onSnipChange={this.handleSnipChange} renderer={this.state.brew.renderer} userThemes={this.props.userThemes} themeBundle={this.state.themeBundle} - snippetBundle={this.state.themeBundle.snippets} onCursorPageChange={this.handleEditorCursorPageChange} onViewPageChange={this.handleEditorViewPageChange} currentEditorViewPageNum={this.state.currentEditorViewPageNum} diff --git a/package-lock.json b/package-lock.json index df134179a..4633936df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "marked-emoji": "^2.0.0", "marked-extended-tables": "^2.0.1", "marked-gfm-heading-id": "^4.0.1", + "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.3", "markedLegacy": "npm:marked@^0.3.19", @@ -73,7 +74,7 @@ }, "engines": { "node": "^20.18.x", - "npm": "^10.2.x" + "npm": "^10.8.x" } }, "node_modules/@ampproject/remapping": { @@ -143,13 +144,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -392,12 +393,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -549,7 +550,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", - "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, @@ -1678,9 +1678,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz", - "integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1689,30 +1690,30 @@ } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1730,9 +1731,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -1947,19 +1948,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@googleapis/drive": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-11.0.0.tgz", @@ -3262,6 +3276,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -3323,6 +3338,7 @@ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" @@ -3419,6 +3435,7 @@ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -3454,6 +3471,7 @@ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", @@ -3517,6 +3535,7 @@ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3571,6 +3590,7 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -3737,6 +3757,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.3.2.tgz", "integrity": "sha512-902o4GiQqI1GqAXfD5rEoz0PJamUfJ3VllpdWaNsFTwdaNjFSFHawvBO+cp5K2j+g2h3bZ4lnM1Xb6yFYGihtA==", "dev": true, + "license": "BSD", "dependencies": { "@babel/template": "^7.25.9", "tslib": "^2.8.1" @@ -4243,20 +4264,20 @@ } }, "node_modules/cacheable": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.9.tgz", - "integrity": "sha512-FicwAUyWnrtnd4QqYAoRlNs44/a1jTL7XDKqm5gJ90wz1DQPlC7U2Rd1Tydpv+E7WAr4sQHuw8Q8M3nZMAyecQ==", + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.10.tgz", + "integrity": "sha512-0ZnbicB/N2R6uziva8l6O6BieBklArWyiGx4GkwAhLKhSHyQtRfM9T1nx7HHuHDKkYB/efJQhz3QJ6x/YqoZzA==", "dev": true, "license": "MIT", "dependencies": { - "hookified": "^1.7.1", - "keyv": "^5.3.1" + "hookified": "^1.8.1", + "keyv": "^5.3.2" } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.1.tgz", - "integrity": "sha512-13hQT2q2VIwOoaJdJa7nY3J8UVbYtMTJFHnwm9LI+SaQRfUiM6Em9KZeOVTCKbMnGcRIL3NSUFpAdjZCq24nLQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.2.tgz", + "integrity": "sha512-Lji2XRxqqa5Wg+CHLVfFKBImfJZ4pCSccu9eVWK6w4c2SDFLd8JAn1zqTuSFnsxb7ope6rMsnIHfp+eBbRBRZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4273,6 +4294,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -4336,9 +4358,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001699", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", - "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", + "version": "1.0.30001704", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz", + "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==", "funding": [ { "type": "opencollective", @@ -4511,6 +4533,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4521,7 +4544,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/colord": { "version": "2.9.3", @@ -4702,12 +4726,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.40.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", - "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", "license": "MIT", "dependencies": { - "browserslist": "^4.24.3" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -4880,6 +4904,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4925,6 +4950,7 @@ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "dev": true, + "license": "MIT", "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" @@ -5026,6 +5052,7 @@ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -5043,6 +5070,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -5060,6 +5088,7 @@ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5354,6 +5383,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -5388,9 +5418,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.97", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz", - "integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==", + "version": "1.5.118", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.118.tgz", + "integrity": "sha512-yNDUus0iultYyVoEFLnQeei7LOQkL8wg8GQpkPCRrOlJXlcCwa6eGKZkxQ9ciHsqZyYbj8Jd94X1CTPzGm+uIA==", "license": "ISC" }, "node_modules/elliptic": { @@ -5492,6 +5522,7 @@ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.7.tgz", "integrity": "sha512-OygGC8kIcDhXX+6yAZRGLqwi2CmEXCbLQixeGUgYeR+Qwlppqmo7DIDr8XibtEBZp+fJcoYpoatp5qwLMEdcqQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", @@ -5552,6 +5583,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -5570,6 +5602,7 @@ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -5634,6 +5667,7 @@ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", @@ -5727,6 +5761,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" }, @@ -6165,6 +6200,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.2.0.tgz", "integrity": "sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg==", + "license": "MIT", "dependencies": { "parseurl": "^1.3.3", "serve-static": "^1.16.2" @@ -6320,6 +6356,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -6368,7 +6405,8 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { "version": "1.0.16", @@ -6485,6 +6523,7 @@ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.1.3" } @@ -6559,6 +6598,7 @@ "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6574,6 +6614,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6588,6 +6642,7 @@ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -6744,6 +6799,7 @@ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -6949,6 +7005,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6989,6 +7046,7 @@ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7022,6 +7080,7 @@ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.0" }, @@ -7036,6 +7095,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7177,9 +7237,9 @@ } }, "node_modules/hookified": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.7.1.tgz", - "integrity": "sha512-OXcdHsXeOiD7OJ5zvWj8Oy/6RCdLwntAX+wUrfemNcMGn6sux4xbEHi2QXwqePYhjQ/yvxxq2MvCRirdlHscBw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.8.1.tgz", + "integrity": "sha512-GrO2l93P8xCWBSTBX9l2BxI78VU/MAAYag+pG8curS3aBGy0++ZlxrQ7PdUOUVMbn5BwkGb6+eRrnf43ipnFEA==", "dev": true, "license": "MIT" }, @@ -7478,6 +7538,7 @@ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", @@ -7513,6 +7574,7 @@ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -7537,6 +7599,7 @@ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -7552,6 +7615,7 @@ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { "has-bigints": "^1.0.2" }, @@ -7579,6 +7643,7 @@ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -7601,6 +7666,7 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7640,6 +7706,7 @@ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", @@ -7657,6 +7724,7 @@ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" @@ -7704,6 +7772,7 @@ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -7738,6 +7807,7 @@ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -7765,6 +7835,7 @@ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7786,6 +7857,7 @@ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -7826,6 +7898,7 @@ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", @@ -7844,6 +7917,7 @@ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7856,6 +7930,7 @@ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3" }, @@ -7883,6 +7958,7 @@ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" @@ -7899,6 +7975,7 @@ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", @@ -7916,6 +7993,7 @@ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" }, @@ -7931,6 +8009,7 @@ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7943,6 +8022,7 @@ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2" }, @@ -7958,6 +8038,7 @@ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" @@ -7988,7 +8069,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -8118,6 +8200,7 @@ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", @@ -9801,7 +9884,8 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -9935,6 +10019,15 @@ "marked": ">=13 <16" } }, + "node_modules/marked-nonbreaking-spaces": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/marked-nonbreaking-spaces/-/marked-nonbreaking-spaces-1.0.1.tgz", + "integrity": "sha512-CUeFRc6OdMMSJThgOM7WAkUjL59VfSf79urKyKYtH9fs3hnrhC3+syFBimYh4vpvUZmjnXoZX0K6V3vZKmyRWQ==", + "license": "MIT", + "peerDependencies": { + "marked": ">=3 <16" + } + }, "node_modules/marked-smartypants-lite": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/marked-smartypants-lite/-/marked-smartypants-lite-1.0.3.tgz", @@ -9970,6 +10063,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -10000,7 +10094,8 @@ "version": "2.12.2", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "dev": true + "dev": true, + "license": "CC0-1.0" }, "node_modules/media-typer": { "version": "1.1.0", @@ -10466,6 +10561,13 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz", @@ -10793,6 +10895,7 @@ "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -10825,6 +10928,7 @@ "version": "4.1.7", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -10892,6 +10996,7 @@ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -11331,6 +11436,7 @@ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -11839,6 +11945,7 @@ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", @@ -11940,6 +12047,7 @@ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -12023,6 +12131,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -12183,6 +12292,7 @@ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -12231,6 +12341,7 @@ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12494,6 +12605,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -12512,6 +12624,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -12527,6 +12640,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12544,6 +12658,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -12634,6 +12749,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -13010,6 +13126,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", @@ -13048,6 +13165,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -13069,6 +13187,7 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", @@ -13220,6 +13339,7 @@ "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-6.0.0.tgz", "integrity": "sha512-1KqrttqpIrCYFAVQ1/bbgXo7EvvcjmkxxmnzVr+U66Xr2OlrNZqQ5+44Tmct6grCWY6wGTIBh2tSANqcmwIM2g==", "dev": true, + "license": "ISC", "dependencies": { "stylelint-order": "^6.0.4" }, @@ -13319,25 +13439,25 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.0.7", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.7.tgz", - "integrity": "sha512-txsf5fu3anp2ff3+gOJJzRImtrtm/oa9tYLN0iTuINZ++EyVR/nRrg2fKYwvG/pXDofcrvvb0scEbX3NyW/COw==", + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.8.tgz", + "integrity": "sha512-FGXHpfmI4XyzbLd3HQ8cbUcsFGohJpZtmQRHr8z8FxxtCe2PcpgIlVLwIgunqjvRmXypBETvwhV4ptJizA+Y1Q==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.7" + "flat-cache": "^6.1.8" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.7.tgz", - "integrity": "sha512-qwZ4xf1v1m7Rc9XiORly31YaChvKt6oNVHuqqZcoED/7O+ToyNVGobKsIAopY9ODcWpEDKEBAbrSOCBHtNQvew==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.8.tgz", + "integrity": "sha512-R6MaD3nrJAtO7C3QOuS79ficm2pEAy++TgEUD8ii1LVlbcgZ9DtASLkt9B+RZSFCzm7QHDMlXPsqqB6W2Pfr1Q==", "dev": true, "license": "MIT", "dependencies": { "cacheable": "^1.8.9", "flatted": "^3.3.3", - "hookified": "^1.7.1" + "hookified": "^1.8.1" } }, "node_modules/stylelint/node_modules/ignore": { @@ -13345,6 +13465,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -13579,6 +13700,7 @@ "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", @@ -13595,6 +13717,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -13610,7 +13733,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/test-exclude": { "version": "6.0.0", @@ -13951,6 +14075,7 @@ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", @@ -13965,6 +14090,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", @@ -13984,6 +14110,7 @@ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", @@ -14005,6 +14132,7 @@ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", @@ -14055,6 +14183,7 @@ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", @@ -14231,9 +14360,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -14573,6 +14702,25 @@ "node": ">=0.10.0" } }, + "node_modules/watchify/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, "node_modules/watchify/node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -14804,6 +14952,7 @@ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, + "license": "MIT", "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", @@ -14823,6 +14972,7 @@ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", @@ -14850,6 +15000,7 @@ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", @@ -14868,6 +15019,7 @@ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", diff --git a/package.json b/package.json index bcc443398..4eb617d7c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "3.18.1", "type": "module", "engines": { - "npm": "^10.2.x", + "npm": "^10.8.x", "node": "^20.18.x" }, "repository": { @@ -112,6 +112,7 @@ "marked-extended-tables": "^2.0.1", "marked-gfm-heading-id": "^4.0.1", "marked-alignment-paragraphs": "^1.0.0", + "marked-nonbreaking-spaces": "^1.0.1", "marked-smartypants-lite": "^1.0.3", "marked-subsuper-text": "^1.0.3", "markedLegacy": "npm:marked@^0.3.19", diff --git a/server/admin.api.js b/server/admin.api.js index e70268afa..a3d7622f1 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -1,3 +1,4 @@ +/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ import { model as HomebrewModel } from './homebrew.model.js'; import { model as NotificationModel } from './notifications.model.js'; import express from 'express'; @@ -11,6 +12,7 @@ import { splitTextStyleAndMetadata } from '../shared/helpers.js'; const router = express.Router(); + process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin'; process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3'; @@ -162,6 +164,180 @@ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{ } }); +// ####################### LOCKS + +router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{ + + const countLocksQuery = { + lock : { $exists: true } + }; + const count = await HomebrewModel.countDocuments(countLocksQuery) + .catch((error)=>{ + throw { name: 'Lock Count Error', message: 'Unable to get lock count', status: 500, HBErrorCode: '61', error }; + }); + + return res.json({ count }); + +})); + +router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{ + const countLocksPipeline = [ + { + $match : + { + 'lock' : { '$exists': 1 } + }, + }, + { + $project : { + shareId : 1, + editId : 1, + title : 1, + lock : 1 + } + } + ]; + const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline) + .catch((error)=>{ + throw { name: 'Can Not Get Locked Brews', message: 'Unable to get locked brew collection', status: 500, HBErrorCode: '68', error }; + }); + return res.json({ + lockedDocuments + }); + +})); + +router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{ + + const lock = req.body; + + lock.applied = new Date; + + const filter = { + shareId : req.params.id + }; + + const brew = await HomebrewModel.findOne(filter); + + if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' }; + + if(brew.lock && !lock.overwrite) { + throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' }; + } + + lock.overwrite = undefined; + + brew.lock = lock; + brew.markModified('lock'); + + await brew.save() + .catch((error)=>{ + throw { name: 'Lock Error', message: 'Unable to set lock', shareId: req.params.id, status: 500, HBErrorCode: '62', error }; + }); + + return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock }); + +})); + +router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{ + + const filter = { + shareId : req.params.id + }; + + const brew = await HomebrewModel.findOne(filter); + + if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' }; + + if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' }; + + brew.lock = undefined; + brew.markModified('lock'); + + await brew.save() + .catch((error)=>{ + throw { name: 'Cannot Unlock', message: 'Unable to clear lock', shareId: req.params.id, status: 500, HBErrorCode: '65', error }; + }); + + return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` }); +})); + +router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{ + const countReviewsPipeline = [ + { + $match : + { + 'lock.reviewRequested' : { '$exists': 1 } + }, + }, + { + $project : { + shareId : 1, + editId : 1, + title : 1, + lock : 1 + } + } + ]; + const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline) + .catch((error)=>{ + throw { name: 'Can Not Get Reviews', message: 'Unable to get review collection', status: 500, HBErrorCode: '68', error }; + }); + return res.json({ + reviewDocuments + }); + +})); + +router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{ + // === This route is NOT Admin only === + // Any user can request a review of their document + const filter = { + shareId : req.params.id, + lock : { $exists: 1 } + }; + + const brew = await HomebrewModel.findOne(filter); + if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; }; + + if(brew.lock.reviewRequested){ + throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' }; + }; + + brew.lock.reviewRequested = new Date(); + brew.markModified('lock'); + + await brew.save() + .catch((error)=>{ + throw { name: 'Can Not Set Review Request', message: `Unable to set request for review on brew ID ${req.params.id}`, code: 500, HBErrorCode: '69', error }; + }); + + return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` }); + +})); + +router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{ + + const filter = { + shareId : req.params.id, + 'lock.reviewRequested' : { $exists: 1 } + }; + + const brew = await HomebrewModel.findOne(filter); + if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; }; + + brew.lock.reviewRequested = undefined; + brew.markModified('lock'); + + await brew.save() + .catch((error)=>{ + throw { name: 'Can Not Clear Review Request', message: `Unable to remove request for review on brew ID ${req.params.id}`, HBErrorCode: '72', error }; + }); + + return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` }); + +})); + // ####################### NOTIFICATIONS router.get('/admin/notification/all', async (req, res, next)=>{ diff --git a/server/admin.api.spec.js b/server/admin.api.spec.js index 036c238c9..e156c6c8f 100644 --- a/server/admin.api.spec.js +++ b/server/admin.api.spec.js @@ -1,6 +1,8 @@ +/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/ import supertest from 'supertest'; import HBApp from './app.js'; import { model as NotificationModel } from './notifications.model.js'; +import { model as HomebrewModel } from './homebrew.model.js'; // Mimic https responses to avoid being redirected all the time @@ -114,4 +116,590 @@ describe('Tests for admin api', ()=>{ expect(response.body).toEqual({ message: 'Notification not found' }); }); }); + + describe('Locks', ()=>{ + describe('Count', ()=>{ + it('Count of all locked documents', async ()=>{ + const testNumber = 16777216; // 8^8, because why not + + jest.spyOn(HomebrewModel, 'countDocuments') + .mockImplementationOnce(()=>{ + return Promise.resolve(testNumber); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .get('/api/lock/count'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ count: testNumber }); + }); + + it('Handle error while fetching count of locked documents', async ()=>{ + jest.spyOn(HomebrewModel, 'countDocuments') + .mockImplementationOnce(()=>{ + return Promise.reject(); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .get('/api/lock/count'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '61', + message : 'Unable to get lock count', + name : 'Lock Count Error', + originalUrl : '/api/lock/count', + status : 500, + }); + }); + }); + + describe('Lists', ()=>{ + it('Get list of all locked documents', async ()=>{ + const testLocks = ['a', 'b']; + + jest.spyOn(HomebrewModel, 'aggregate') + .mockImplementationOnce(()=>{ + return Promise.resolve(testLocks); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .get('/api/locks'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ lockedDocuments: testLocks }); + }); + + it('Handle error while fetching list of all locked documents', async ()=>{ + jest.spyOn(HomebrewModel, 'aggregate') + .mockImplementationOnce(()=>{ + return Promise.reject(); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .get('/api/locks'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '68', + message : 'Unable to get locked brew collection', + name : 'Can Not Get Locked Brews', + originalUrl : '/api/locks', + status : 500 + }); + }); + + it('Get list of all locked documents with pending review requests', async ()=>{ + const testLocks = ['a', 'b']; + + jest.spyOn(HomebrewModel, 'aggregate') + .mockImplementationOnce(()=>{ + return Promise.resolve(testLocks); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .get('/api/lock/reviews'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ reviewDocuments: testLocks }); + }); + + it('Handle error while fetching list of all locked documents with pending review requests', async ()=>{ + jest.spyOn(HomebrewModel, 'aggregate') + .mockImplementationOnce(()=>{ + return Promise.reject(); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .get('/api/lock/reviews'); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '68', + message : 'Unable to get review collection', + name : 'Can Not Get Reviews', + originalUrl : '/api/lock/reviews', + status : 500 + }); + }); + }); + + describe('Lock', ()=>{ + it('Lock a brew', async ()=>{ + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); } + }; + + const testLock = { + code : 999, + editMessage : 'edit', + shareMessage : 'share' + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .post(`/api/lock/${testBrew.shareId}`) + .send(testLock); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ + applied : expect.any(String), + code : testLock.code, + editMessage : testLock.editMessage, + shareMessage : testLock.shareMessage, + name : 'LOCKED', + message : `Lock applied to brew ID ${testBrew.shareId} - ${testBrew.title}` + }); + }); + + it('Overwrite lock on a locked brew', async ()=>{ + const testLock = { + code : 999, + editMessage : 'newEdit', + shareMessage : 'newShare', + overwrite : true + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); }, + lock : { + code : 1, + editMessage : 'oldEdit', + shareMessage : 'oldShare', + } + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .post(`/api/lock/${testBrew.shareId}`) + .send(testLock); + + expect(response.status).toBe(200); + expect(response.body).toMatchObject({ + applied : expect.any(String), + code : testLock.code, + editMessage : testLock.editMessage, + shareMessage : testLock.shareMessage, + name : 'LOCKED', + message : `Lock applied to brew ID ${testBrew.shareId} - ${testBrew.title}` + }); + }); + + it('Error when locking a locked brew', async ()=>{ + const testLock = { + code : 999, + editMessage : 'newEdit', + shareMessage : 'newShare' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); }, + lock : { + code : 1, + editMessage : 'oldEdit', + shareMessage : 'oldShare', + } + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .post(`/api/lock/${testBrew.shareId}`) + .send(testLock); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '64', + message : 'Lock already exists on brew', + name : 'Already Locked', + originalUrl : `/api/lock/${testBrew.shareId}`, + shareId : testBrew.shareId, + status : 500, + title : 'title' + }); + }); + + it('Handle save error while locking a brew', async ()=>{ + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.reject(); } + }; + + const testLock = { + code : 999, + editMessage : 'edit', + shareMessage : 'share' + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .post(`/api/lock/${testBrew.shareId}`) + .send(testLock); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '62', + message : 'Unable to set lock', + name : 'Lock Error', + originalUrl : `/api/lock/${testBrew.shareId}`, + shareId : testBrew.shareId, + status : 500 + }); + }); + }); + + describe('Unlock', ()=>{ + it('Unlock a brew', async ()=>{ + const testLock = { + applied : 'YES', + code : 999, + editMessage : 'edit', + shareMessage : 'share' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); }, + lock : testLock + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .put(`/api/unlock/${testBrew.shareId}`); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + name : 'Unlocked', + message : `Lock removed from brew ID ${testBrew.shareId}` + }); + }); + + it('Error when unlocking a brew with no lock', async ()=>{ + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); }, + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .put(`/api/unlock/${testBrew.shareId}`); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '67', + message : 'Cannot unlock as brew is not locked', + name : 'Not Locked', + originalUrl : `/api/unlock/${testBrew.shareId}`, + shareId : testBrew.shareId, + status : 500, + }); + }); + + it('Handle error while unlocking a brew', async ()=>{ + const testLock = { + applied : 'YES', + code : 999, + editMessage : 'edit', + shareMessage : 'share' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.reject(); }, + lock : testLock + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .put(`/api/unlock/${testBrew.shareId}`); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '65', + message : 'Unable to clear lock', + name : 'Cannot Unlock', + originalUrl : `/api/unlock/${testBrew.shareId}`, + shareId : testBrew.shareId, + status : 500 + }); + }); + }); + + describe('Reviews', ()=>{ + it('Add review request to a locked brew', async ()=>{ + const testLock = { + applied : 'YES', + code : 999, + editMessage : 'edit', + shareMessage : 'share' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); }, + lock : testLock + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .put(`/api/lock/review/request/${testBrew.shareId}`); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message : `Review requested on brew ID ${testBrew.shareId} - ${testBrew.title}`, + name : 'Review Requested', + }); + }); + + it('Error when cannot find a locked brew', async ()=>{ + const testBrew = { + shareId : 'shareId' + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(false); + }); + + + const response = await app + .put(`/api/lock/review/request/${testBrew.shareId}`) + .catch((err)=>{return err;}); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + message : `Cannot find a locked brew with ID ${testBrew.shareId}`, + name : 'Brew Not Found', + HBErrorCode : '70', + code : 500, + originalUrl : `/api/lock/review/request/${testBrew.shareId}` + }); + }); + + it('Error when review is already requested', async ()=>{ + const testLock = { + applied : 'YES', + code : 999, + editMessage : 'edit', + shareMessage : 'share', + reviewRequested : 'YES' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); }, + lock : testLock + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(false); + }); + + + const response = await app + .put(`/api/lock/review/request/${testBrew.shareId}`) + .catch((err)=>{return err;}); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '70', + code : 500, + message : `Cannot find a locked brew with ID ${testBrew.shareId}`, + name : 'Brew Not Found', + originalUrl : `/api/lock/review/request/${testBrew.shareId}` + }); + }); + + it('Handle error while adding review request to a locked brew', async ()=>{ + const testLock = { + applied : 'YES', + code : 999, + editMessage : 'edit', + shareMessage : 'share' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.reject(); }, + lock : testLock + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .put(`/api/lock/review/request/${testBrew.shareId}`); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '69', + code : 500, + message : `Unable to set request for review on brew ID ${testBrew.shareId}`, + name : 'Can Not Set Review Request', + originalUrl : `/api/lock/review/request/${testBrew.shareId}` + }); + }); + + it('Clear review request from a locked brew', async ()=>{ + const testLock = { + applied : 'YES', + code : 999, + editMessage : 'edit', + shareMessage : 'share', + reviewRequested : 'YES' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.resolve(); }, + lock : testLock + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .put(`/api/lock/review/remove/${testBrew.shareId}`); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message : `Review request removed for brew ID ${testBrew.shareId} - ${testBrew.title}`, + name : 'Review Request Cleared' + }); + }); + + it('Error when clearing review request from a brew with no review request', async ()=>{ + const testBrew = { + shareId : 'shareId', + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(false); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .put(`/api/lock/review/remove/${testBrew.shareId}`); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '73', + message : `Brew ID ${testBrew.shareId} does not have a review pending!`, + name : 'Can Not Clear Review Request', + originalUrl : `/api/lock/review/remove/${testBrew.shareId}` + }); + }); + + it('Handle error while clearing review request from a locked brew', async ()=>{ + const testLock = { + applied : 'YES', + code : 999, + editMessage : 'edit', + shareMessage : 'share', + reviewRequested : 'YES' + }; + + const testBrew = { + shareId : 'shareId', + title : 'title', + markModified : ()=>{ return true; }, + save : ()=>{ return Promise.reject(); }, + lock : testLock + }; + + jest.spyOn(HomebrewModel, 'findOne') + .mockImplementationOnce(()=>{ + return Promise.resolve(testBrew); + }); + + const response = await app + .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) + .put(`/api/lock/review/remove/${testBrew.shareId}`); + + expect(response.status).toBe(500); + expect(response.body).toEqual({ + HBErrorCode : '72', + message : `Unable to remove request for review on brew ID ${testBrew.shareId}`, + name : 'Can Not Clear Review Request', + originalUrl : `/api/lock/review/remove/${testBrew.shareId}` + }); + }); + }); + }); }); diff --git a/server/app.js b/server/app.js index 6bd0c7c9a..b7d990baf 100644 --- a/server/app.js +++ b/server/app.js @@ -2,7 +2,7 @@ // Set working directory to project root import { dirname } from 'path'; import { fileURLToPath } from 'url'; -import packageJSON from './../package.json' with { type: 'json' }; +import packageJSON from './../package.json' with { type: 'json' }; const __dirname = dirname(fileURLToPath(import.meta.url)); process.chdir(`${__dirname}/..`); diff --git a/server/homebrew.api.js b/server/homebrew.api.js index af408e579..2ab29f31b 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -8,9 +8,11 @@ import Markdown from '../shared/naturalcrit/markdown.js'; import yaml from 'js-yaml'; import asyncHandler from 'express-async-handler'; import { nanoid } from 'nanoid'; -import { splitTextStyleAndMetadata } from '../shared/helpers.js'; +import { splitTextStyleAndMetadata, + brewSnippetsToJSON } from '../shared/helpers.js'; import checkClientVersion from './middleware/check-client-version.js'; + const router = express.Router(); import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js'; @@ -118,8 +120,8 @@ const api = { throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04' }; } - if(stub?.lock?.locked && accessType != 'edit') { - throw { HBErrorCode: '51', code: stub?.lock.code, message: stub?.lock.shareMessage, brewId: stub?.shareId, brewTitle: stub?.title }; + if(stub?.lock && accessType === 'share') { + throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title, brewAuthors: stub.authors }; } // If there's a google id, get it if requesting the full brew or if no stub found yet @@ -175,6 +177,8 @@ const api = { `${text}`; } const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']); + const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets; + metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined; text = `\`\`\`metadata\n` + `${yaml.dump(metadata)}\n` + `\`\`\`\n\n` + @@ -301,7 +305,7 @@ const api = { themeAuthor ??= currentTheme.authors?.[0]; // If there is anything in the snippets or style members, append them to the appropriate array - if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets)); + if(currentTheme?.snippets) completeSnippets.push({ name: currentTheme.title, snippets: currentTheme.snippets }); if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`); req.params.id = currentTheme.theme; diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index 1c42cb4be..8bb3a0c0b 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -302,7 +302,7 @@ describe('Tests for api', ()=>{ }); it('access is denied to a locked brew', async()=>{ - const lockBrew = { title: 'test brew', shareId: '1', lock: { locked: true, code: 404, shareMessage: 'brew locked' } }; + const lockBrew = { title: 'test brew', shareId: '1', lock: { code: 404, shareMessage: 'brew locked' } }; model.get = jest.fn(()=>toBrewPromise(lockBrew)); api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); @@ -939,7 +939,7 @@ brew`); }); describe('Get CSS', ()=>{ it('should return brew style content as CSS text', async ()=>{ - const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n````\n\n' }; + const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n```\n\n' }; const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew })); api.getId = jest.fn(()=>({ id: '1', googleId: undefined })); @@ -1034,7 +1034,7 @@ brew`); expect(testBrew.theme).toEqual('5ePHB'); expect(testBrew.lang).toEqual('en'); // Style - expect(testBrew.style).toEqual('style\nstyle\nstyle'); + expect(testBrew.style).toEqual('style\nstyle\nstyle\n'); // Text expect(testBrew.text).toEqual('text\n'); }); diff --git a/server/homebrew.model.js b/server/homebrew.model.js index 15341f5d8..2e74b1de2 100644 --- a/server/homebrew.model.js +++ b/server/homebrew.model.js @@ -27,7 +27,9 @@ const HomebrewSchema = mongoose.Schema({ updatedAt : { type: Date, default: Date.now }, lastViewed : { type: Date, default: Date.now }, views : { type: Number, default: 0 }, - version : { type: Number, default: 1 } + version : { type: Number, default: 1 }, + + lock : { type: Object } }, { versionKey: false }); HomebrewSchema.statics.increaseView = async function(query) { diff --git a/shared/helpers.js b/shared/helpers.js index 2ad9218fa..0ca681dfb 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -2,24 +2,103 @@ import _ from 'lodash'; import yaml from 'js-yaml'; import request from '../client/homebrew/utils/request-middleware.js'; +// Convert the templates from a brew to a Snippets Structure. +const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=null, full=true)=>{ + const textSplit = /^(\\snippet +.+\n)/gm; + const mpAsSnippets = []; + // Snippets from Themes first. + if(themeBundleSnippets) { + for (let themes of themeBundleSnippets) { + if(typeof themes !== 'string') { + const userSnippets = []; + const snipSplit = themes.snippets.trim().split(textSplit).slice(1); + for (let snips = 0; snips < snipSplit.length; snips+=2) { + if(!snipSplit[snips].startsWith('\\snippet ')) break; + const snippetName = snipSplit[snips].split(/\\snippet +/)[1].split('\n')[0].trim(); + if(snippetName.length != 0) { + userSnippets.push({ + name : snippetName, + icon : '', + gen : snipSplit[snips + 1], + }); + } + } + if(userSnippets.length > 0) { + mpAsSnippets.push({ + name : themes.name, + icon : '', + gen : '', + subsnippets : userSnippets + }); + } + } + } + } + // Local Snippets + if(userBrewSnippets) { + const userSnippets = []; + const snipSplit = userBrewSnippets.trim().split(textSplit).slice(1); + for (let snips = 0; snips < snipSplit.length; snips+=2) { + if(!snipSplit[snips].startsWith('\\snippet ')) break; + const snippetName = snipSplit[snips].split(/\\snippet +/)[1].split('\n')[0].trim(); + if(snippetName.length != 0) { + const subSnip = { + name : snippetName, + gen : snipSplit[snips + 1], + }; + // if(full) subSnip.icon = ''; + userSnippets.push(subSnip); + } + } + if(userSnippets.length) { + mpAsSnippets.push({ + name : menuTitle, + // icon : '', + subsnippets : userSnippets + }); + } + } + + const returnObj = { + snippets : mpAsSnippets + }; + + if(full) { + returnObj.groupName = 'Brew Snippets'; + returnObj.icon = 'fas fa-th-list'; + returnObj.view = 'text'; + } + + return returnObj; +}; + +const yamlSnippetsToText = (yamlObj)=>{ + if(typeof yamlObj == 'string') return yamlObj; + + let snippetsText = ''; + + for (let snippet of yamlObj) { + for (let subSnippet of snippet.subsnippets) { + snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`; + } + } + return snippetsText; +}; + 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 index = brew.text.indexOf('\n```\n\n'); + const metadataSection = brew.text.slice(11, 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); + brew.snippets = yamlSnippetsToText(_.pick(metadata, ['snippets']).snippets || ''); + brew.text = brew.text.slice(index + 6); } 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); - } - if(brew.text.startsWith('```snippets')) { - const index = brew.text.indexOf('```\n\n'); - brew.snippets = brew.text.slice(11, index - 1); - brew.text = brew.text.slice(index + 5); + const index = brew.text.indexOf('\n```\n\n'); + brew.style = brew.text.slice(7, index + 1); + brew.text = brew.text.slice(index + 6); } // Handle old brews that still have empty strings in the tags metadata @@ -64,4 +143,5 @@ export { splitTextStyleAndMetadata, printCurrentBrew, fetchThemeBundle, + brewSnippetsToJSON }; diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 1e1b4a001..e00bca205 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -7,7 +7,8 @@ import MarkedExtendedTables from 'marked-extended-tables'; import { markedSmartypantsLite as MarkedSmartypantsLite } from 'marked-smartypants-lite'; import { gfmHeadingId as MarkedGFMHeadingId, resetHeadings as MarkedGFMResetHeadingIDs } from 'marked-gfm-heading-id'; import { markedEmoji as MarkedEmojis } from 'marked-emoji'; -import MarkedAlignedParagraphs from 'marked-alignment-paragraphs'; +import MarkedAlignedParagraphs from 'marked-alignment-paragraphs'; +import MarkedNonbreakingSpaces from 'marked-nonbreaking-spaces'; import MarkedSubSuperText from 'marked-subsuper-text'; import { romanize } from 'romans'; import writtenNumber from 'written-number'; @@ -409,27 +410,6 @@ const forcedParagraphBreaks = { } }; -const nonbreakingSpaces = { - name : 'nonbreakingSpaces', - level : 'inline', - start(src) { return src.match(/:>+/m)?.index; }, // Hint to Marked.js to stop and check for a match - tokenizer(src, tokens) { - const regex = /:(>+)/ym; - const match = regex.exec(src); - if(match?.length) { - return { - type : 'nonbreakingSpaces', // Should match "name" above - raw : match[0], // Text to consume from the source - length : match[1].length, - text : '' - }; - } - }, - renderer(token) { - return ` `.repeat(token.length).concat(''); - } -}; - const definitionListsSingleLine = { name : 'definitionListsSingleLine', level : 'block', @@ -785,11 +765,12 @@ const tableTerminators = [ ]; Marked.use(MarkedVariables()); -Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, +Marked.use({ extensions : [justifiedParagraphs, definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, nonbreakingSpaces, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use(mustacheInjectBlock); Marked.use(MarkedAlignedParagraphs()); Marked.use(MarkedSubSuperText()); +Marked.use(MarkedNonbreakingSpaces()); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use(MarkedExtendedTables({ interruptPatterns: tableTerminators }), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); diff --git a/tests/markdown/non-breaking-spaces.test.js b/tests/markdown/non-breaking-spaces.test.js index 83fb55509..e498645a6 100644 --- a/tests/markdown/non-breaking-spaces.test.js +++ b/tests/markdown/non-breaking-spaces.test.js @@ -2,55 +2,7 @@ import Markdown from 'naturalcrit/markdown.js'; -describe('Non-Breaking Spaces', ()=>{ - test('Single Space', function() { - const source = ':>\n\n'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

 

`); - }); - - test('Double Space', function() { - const source = ':>>\n\n'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

  

`); - }); - - test('Triple Space', function() { - const source = ':>>>\n\n'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

   

`); - }); - - test('Many Space', function() { - const source = ':>>>>>>>>>>\n\n'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

          

`); - }); - - test('Multiple sets of Spaces', function() { - const source = ':>>>\n:>>>\n:>>>'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

   \n   \n   

`); - }); - - test('Pair of inline Spaces', function() { - const source = ':>>:>>'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

    

`); - }); - - test('Space directly between two paragraphs', function() { - const source = 'Line 1\n:>>\nLine 2'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

Line 1\n  \nLine 2

`); - }); - - test('Ignored inside a code block', function() { - const source = '```\n\n:>\n\n```\n'; - const rendered = Markdown.render(source).trim(); - expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
\n:>\n
`); - }); - +describe('Non-Breaking Spaces Interactions', ()=>{ test('I am actually a single-line definition list!', function() { const source = 'Term ::> Definition 1\n'; const rendered = Markdown.render(source).trim();