mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-06 01:22:44 +00:00
Merge branch 'master' into pr/3845
This commit is contained in:
@@ -70,9 +70,15 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Hard Breaks
|
name: Test - Hard Breaks
|
||||||
command: npm run test:hard-breaks
|
command: npm run test:hard-breaks
|
||||||
|
- run:
|
||||||
|
name: Test - Non-Breaking Spaces
|
||||||
|
command: npm run test:non-breaking-spaces
|
||||||
- run:
|
- run:
|
||||||
name: Test - Variables
|
name: Test - Variables
|
||||||
command: npm run test:variables
|
command: npm run test:variables
|
||||||
|
- run:
|
||||||
|
name: Test - Emojis
|
||||||
|
command: npm run test:emojis
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
@@ -82,6 +88,9 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Coverage
|
name: Test - Coverage
|
||||||
command: npm run test:coverage
|
command: npm run test:coverage
|
||||||
|
- run:
|
||||||
|
name: Test - Content Negotiation
|
||||||
|
command: npm run test:content-negotiation
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
build_and_test:
|
build_and_test:
|
||||||
|
|||||||
25
.github/pull_request_template.md
vendored
25
.github/pull_request_template.md
vendored
@@ -1,26 +1,29 @@
|
|||||||
<!--
|
> [!TIP]
|
||||||
Before submitting a Pull Request, please consider the following to speed up reviews:
|
> Before submitting a Pull Request, please consider the following to speed up reviews:
|
||||||
- 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
> - 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
||||||
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
> - 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
||||||
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
> - 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
||||||
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
> - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||||
-->
|
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
|
_Describe what your PR accomplishes. Consider walking through the main changes to aid reviewers in following your code, especially if it covers multiple files._
|
||||||
|
|
||||||
## Related Issues or Discussions
|
## Related Issues or Discussions
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> If no issue exists yet, create it, and get agreement on the approach (or paste in a previous agreement from chat, etc.) before moving forward. (Experimental PRs are OK without prior discussion, but do not expect to get merged.)
|
||||||
|
|
||||||
- Closes #
|
- Closes #
|
||||||
|
|
||||||
## QA Instructions, Screenshots, Recordings
|
## QA Instructions, Screenshots, Recordings
|
||||||
|
|
||||||
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
_Replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||||
images for UI changes._
|
screenshots or recordings for UI changes._
|
||||||
|
|
||||||
### Reviewer Checklist
|
### Reviewer Checklist
|
||||||
|
|
||||||
_Please replace the list below with specific features you want reviewers to look at._
|
_Replace the list below with specific features you want reviewers to look at._
|
||||||
|
|
||||||
*Reviewers, refer to this list when testing features, or suggest new items *
|
*Reviewers, refer to this list when testing features, or suggest new items *
|
||||||
- [ ] Verify new features are functional
|
- [ ] Verify new features are functional
|
||||||
@@ -32,5 +35,3 @@ _Please replace the list below with specific features you want reviewers to look
|
|||||||
- [ ] Feature A handles negative numbers
|
- [ ] Feature A handles negative numbers
|
||||||
- [ ] Identify opportunities for simplification and refactoring
|
- [ ] Identify opportunities for simplification and refactoring
|
||||||
- [ ] Check for code legibility and appropriate comments
|
- [ ] Check for code legibility and appropriate comments
|
||||||
|
|
||||||
<details><summary>Copy this list</summary>
|
|
||||||
|
|||||||
@@ -115,10 +115,10 @@
|
|||||||
color : #D3D3D3;
|
color : #D3D3D3;
|
||||||
accent-color : #D3D3D3;
|
accent-color : #D3D3D3;
|
||||||
|
|
||||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
&::-webkit-slider-thumb, &::-moz-range-thumb {
|
||||||
width : 5px;
|
width : 5px;
|
||||||
height : 5px;
|
height : 5px;
|
||||||
cursor : pointer;
|
cursor : ew-resize;
|
||||||
outline : none;
|
outline : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,19 +207,11 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
const foldButtons = <>
|
|
||||||
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.foldCode} >
|
|
||||||
<i className='fas fa-compress-alt' />
|
|
||||||
</div>
|
|
||||||
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
|
|
||||||
onClick={this.props.unfoldCode} >
|
|
||||||
<i className='fas fa-expand-alt' />
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
return <div className='editors'>
|
return (
|
||||||
<div className='historyTools'>
|
<div className='editors'>
|
||||||
|
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||||
onClick={this.toggleHistoryMenu} >
|
onClick={this.toggleHistoryMenu} >
|
||||||
<i className='fas fa-clock-rotate-left' />
|
<i className='fas fa-clock-rotate-left' />
|
||||||
@@ -235,14 +227,21 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='codeTools'>
|
<div className='codeTools'>
|
||||||
{foldButtons}
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
onClick={this.props.foldCode} >
|
||||||
|
<i className='fas fa-compress-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
|
onClick={this.props.unfoldCode} >
|
||||||
|
<i className='fas fa-expand-alt' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
onClick={this.toggleThemeSelector} >
|
onClick={this.toggleThemeSelector} >
|
||||||
<i className='fas fa-palette' />
|
<i className='fas fa-palette' />
|
||||||
{this.state.themeSelector && this.renderThemeSelector()}
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div></>}
|
||||||
|
|
||||||
|
|
||||||
<div className='tabs'>
|
<div className='tabs'>
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
@@ -259,7 +258,8 @@ const Snippetbar = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>;
|
</div>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
justify-content : flex-end;
|
justify-content : flex-end;
|
||||||
min-width : 225px;
|
min-width : 225px;
|
||||||
|
|
||||||
&:only-child { margin-left : auto; }
|
&:only-child { margin-left : auto;min-width:unset;}
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
display : flex;
|
display : flex;
|
||||||
@@ -38,6 +38,11 @@
|
|||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|
||||||
|
&.editorTool:not(.active) {
|
||||||
|
cursor:not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,&.selected { background-color : #999999; }
|
&:hover,&.selected { background-color : #999999; }
|
||||||
&.text {
|
&.text {
|
||||||
.tooltipLeft('Brew Editor');
|
.tooltipLeft('Brew Editor');
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
//╔===--------------- Polyfills --------------===╗//
|
||||||
|
import 'core-js/es/string/to-well-formed.js';
|
||||||
|
//╚===--------------- ---------------===╝//
|
||||||
|
|
||||||
require('./homebrew.less');
|
require('./homebrew.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const { StaticRouter:Router } = require('react-router-dom/server');
|
const { StaticRouter:Router } = require('react-router');
|
||||||
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
|
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
|
|||||||
@@ -18,7 +18,18 @@ const errorIndex = (props)=>{
|
|||||||
'01' : dedent`
|
'01' : dedent`
|
||||||
## An error occurred while retrieving this brew from Google Drive!
|
## An error occurred while retrieving this brew from Google Drive!
|
||||||
|
|
||||||
Google reported an error while attempting to retrieve a brew from this link.`,
|
Google is able to see the brew at this link, but reported an error while attempting to retrieve it.
|
||||||
|
|
||||||
|
### Refreshing your Google Credentials
|
||||||
|
|
||||||
|
This issue is likely caused by an issue with your Google credentials; if you are the owner of this file, the following steps may resolve the issue:
|
||||||
|
|
||||||
|
- Go to https://www.naturalcrit.com/login and click logout if present (in small text at the bottom of the page).
|
||||||
|
- Click "Sign In with Google", which will refresh your Google credentials.
|
||||||
|
- After completing the sign in process, return to Homebrewery and refresh/reload the page so that it can pick up the updated credentials.
|
||||||
|
- If this was the source of the issue, it should now be resolved.
|
||||||
|
|
||||||
|
If following these steps does not resolve the issue, please let us know!`,
|
||||||
|
|
||||||
// Google Drive - 404 : brew deleted or access denied
|
// Google Drive - 404 : brew deleted or access denied
|
||||||
'02' : dedent`
|
'02' : dedent`
|
||||||
@@ -50,7 +61,7 @@ const errorIndex = (props)=>{
|
|||||||
- **The Google Account may be closed.** Google may have removed the account
|
- **The Google Account may be closed.** Google may have removed the account
|
||||||
due to inactivity or violating a Google policy. Make sure the owner can
|
due to inactivity or violating a Google policy. Make sure the owner can
|
||||||
still access Google Drive normally and upload/download files to it.
|
still access Google Drive normally and upload/download files to it.
|
||||||
:
|
|
||||||
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
||||||
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
||||||
You can also find the Activity tab on the right side of the Google Drive page, which
|
You can also find the Activity tab on the right side of the Google Drive page, which
|
||||||
@@ -172,8 +183,8 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle}`,
|
**Brew Title:** ${props.brew.brewTitle}`,
|
||||||
|
|
||||||
// ####### Admin page error #######
|
// ####### Admin page error #######
|
||||||
'52': dedent`
|
'52' : dedent`
|
||||||
## Access Denied
|
## Access Denied
|
||||||
You need to provide correct administrator credentials to access this page.`,
|
You need to provide correct administrator credentials to access this page.`,
|
||||||
|
|
||||||
|
|||||||
1100
package-lock.json
generated
1100
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -27,6 +27,7 @@
|
|||||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||||
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
||||||
|
"test:content-negotiation": "jest \"server/middleware/.*.spec.js\" --verbose",
|
||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||||
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||||
|
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace",
|
||||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
||||||
@@ -91,9 +93,11 @@
|
|||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
|
"core-js": "^3.39.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"dompurify": "^3.2.2",
|
"dompurify": "^3.2.3",
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
@@ -111,13 +115,13 @@
|
|||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.8.4",
|
"mongoose": "^8.9.2",
|
||||||
"nanoid": "5.0.9",
|
"nanoid": "5.0.9",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.28.0",
|
"react-router": "^7.0.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.1",
|
"superagent": "^10.1.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
@@ -125,15 +129,15 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"@stylistic/stylelint-plugin": "^3.1.1",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||||
"eslint": "^9.16.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-jest": "^28.9.0",
|
"eslint-plugin-jest": "^28.10.0",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"globals": "^15.13.0",
|
"globals": "^15.14.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.11.0",
|
"stylelint": "^16.12.0",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^5.1.1",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^14.0.1",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0"
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ const { pack, watchFile, livereload } = vitreum;
|
|||||||
import lessTransform from 'vitreum/transforms/less.js';
|
import lessTransform from 'vitreum/transforms/less.js';
|
||||||
import assetTransform from 'vitreum/transforms/asset.js';
|
import assetTransform from 'vitreum/transforms/asset.js';
|
||||||
import babel from '@babel/core';
|
import babel from '@babel/core';
|
||||||
|
import babelConfig from '../babel.config.json' with { type : 'json' };
|
||||||
import less from 'less';
|
import less from 'less';
|
||||||
|
|
||||||
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
||||||
|
|
||||||
const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
|
||||||
|
|
||||||
const transforms = {
|
const transforms = {
|
||||||
'.js' : (code, filename, opts)=>babelify(code),
|
'.js' : (code, filename, opts)=>babelify(code),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
import {model as NotificationModel } from './notifications.model.js';
|
import { model as NotificationModel } from './notifications.model.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import Moment from 'moment';
|
import Moment from 'moment';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
@@ -108,6 +108,9 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin',
|
|||||||
|
|
||||||
req.body = brew;
|
req.body = brew;
|
||||||
|
|
||||||
|
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||||
|
req.account = undefined;
|
||||||
|
|
||||||
return await HomebrewAPI.updateBrew(req, res);
|
return await HomebrewAPI.updateBrew(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
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));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
@@ -26,7 +26,7 @@ import serveCompressedStaticAssets from './static-assets.mv.js';
|
|||||||
import sanitizeFilename from 'sanitize-filename';
|
import sanitizeFilename from 'sanitize-filename';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import templateFn from '../client/template.js';
|
import templateFn from '../client/template.js';
|
||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
|
|
||||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||||
@@ -47,7 +47,7 @@ const sanitizeBrew = (brew, accessType)=>{
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
app.set('trust proxy', 1 /* number of proxies between user and server */)
|
app.set('trust proxy', 1 /* number of proxies between user and server */);
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
app.use(contentNegotiation);
|
app.use(contentNegotiation);
|
||||||
@@ -55,6 +55,40 @@ app.use(bodyParser.json({ limit: '25mb' }));
|
|||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(forceSSL);
|
app.use(forceSSL);
|
||||||
|
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
const nodeEnv = config.get('node_env');
|
||||||
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
|
|
||||||
|
const corsOptions = {
|
||||||
|
origin : (origin, callback)=>{
|
||||||
|
|
||||||
|
const allowedOrigins = [
|
||||||
|
'https://homebrewery.naturalcrit.com',
|
||||||
|
'https://www.naturalcrit.com',
|
||||||
|
'https://naturalcrit-stage.herokuapp.com',
|
||||||
|
'https://homebrewery-stage.herokuapp.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
if(isLocalEnvironment) {
|
||||||
|
allowedOrigins.push('http://localhost:8000', 'http://localhost:8010');
|
||||||
|
}
|
||||||
|
|
||||||
|
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
||||||
|
|
||||||
|
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin)) {
|
||||||
|
callback(null, true);
|
||||||
|
} else {
|
||||||
|
console.log(origin, 'not allowed');
|
||||||
|
callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
credentials : true,
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use(cors(corsOptions));
|
||||||
|
|
||||||
//Account Middleware
|
//Account Middleware
|
||||||
app.use((req, res, next)=>{
|
app.use((req, res, next)=>{
|
||||||
if(req.cookies && req.cookies.nc_session){
|
if(req.cookies && req.cookies.nc_session){
|
||||||
@@ -62,7 +96,9 @@ app.use((req, res, next)=>{
|
|||||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||||
//console.log("Just loaded up JWT from cookie:");
|
//console.log("Just loaded up JWT from cookie:");
|
||||||
//console.log(req.account);
|
//console.log(req.account);
|
||||||
} catch (e){}
|
} catch (e){
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.config = {
|
req.config = {
|
||||||
@@ -273,7 +309,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
brews.forEach(brew => brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
if(ownAccount && req?.account?.googleId){
|
||||||
const auth = await GoogleActions.authCheck(req.account, res);
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
@@ -312,6 +348,34 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Change author name on brews
|
||||||
|
app.put('/api/user/rename', async (req, res)=>{
|
||||||
|
const { username, newUsername } = req.body;
|
||||||
|
const ownAccount = req.account && (req.account.username == newUsername);
|
||||||
|
|
||||||
|
if(!username || !newUsername)
|
||||||
|
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||||
|
if(!ownAccount)
|
||||||
|
return res.status(403).json({ error: 'Must be logged in to change your username' });
|
||||||
|
try {
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, ['authors']);
|
||||||
|
const renamePromises = brews.map(async (brew)=>{
|
||||||
|
const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
|
||||||
|
);
|
||||||
|
return HomebrewModel.updateOne(
|
||||||
|
{ _id: brew._id },
|
||||||
|
{ $set: { authors: updatedAuthors } }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await Promise.all(renamePromises);
|
||||||
|
|
||||||
|
return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error renaming brews:', error);
|
||||||
|
return res.status(500).json({ error: 'Failed to rename brews.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
@@ -399,7 +463,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
|||||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||||
const data = {};
|
const data = {};
|
||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
if(!req.account) {
|
if(!req.account) {
|
||||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||||
const error = new Error('No valid account');
|
const error = new Error('No valid account');
|
||||||
@@ -413,7 +477,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
let googleCount = [];
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
auth = await GoogleActions.authCheck(req.account, res, false)
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
|
|
||||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -448,8 +512,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
|
||||||
// Local only
|
// Local only
|
||||||
if(isLocalEnvironment){
|
if(isLocalEnvironment){
|
||||||
// Login
|
// Login
|
||||||
@@ -477,8 +539,8 @@ app.get('/vault', asyncHandler(async(req, res, next)=>{
|
|||||||
|
|
||||||
//Send rendered page
|
//Send rendered page
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||||
|
|
||||||
const page = await renderPage(req, res);
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
res.send(page);
|
res.send(page);
|
||||||
|
|||||||
@@ -106,12 +106,12 @@ const api = {
|
|||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
googleId ??= stub?.googleId;
|
googleId ??= stub?.googleId;
|
||||||
|
|
||||||
const isOwner = stub?.authors?.length === 0 || stub?.authors?.[0] === req.account?.username;
|
const isOwner = (accessType == 'edit' && (!stub || stub?.authors?.length === 0)) || stub?.authors?.[0] === req.account?.username;
|
||||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
|
||||||
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
||||||
const accessError = { name: 'Access Error', status: 401, authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
const accessError = { name: 'Access Error', status: 401, authors: stub?.authors, brewTitle: stub?.title, shareId: stub?.shareId };
|
||||||
if(req.account)
|
if(req.account)
|
||||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
||||||
else
|
else
|
||||||
@@ -119,13 +119,13 @@ const api = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(stub?.lock?.locked && accessType != 'edit') {
|
if(stub?.lock?.locked && accessType != 'edit') {
|
||||||
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
throw { HBErrorCode: '51', code: stub?.lock.code, message: stub?.lock.shareMessage, brewId: stub?.shareId, brewTitle: stub?.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there's a google id, get it if requesting the full brew or if no stub found yet
|
||||||
if(!stubOnly && googleId) {
|
if(googleId && (!stubOnly || !stub)) {
|
||||||
const oAuth2Client = isOwner? GoogleActions.authCheck(req.account, res) : undefined;
|
const oAuth2Client = isOwner ? GoogleActions.authCheck(req.account, res) : undefined;
|
||||||
|
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||||
.catch((googleError)=>{
|
.catch((googleError)=>{
|
||||||
const reason = googleError.errors?.[0].reason;
|
const reason = googleError.errors?.[0].reason;
|
||||||
@@ -467,12 +467,11 @@ const api = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use('/api', checkClientVersion);
|
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||||
router.post('/api', asyncHandler(api.newBrew));
|
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
|
||||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import packageJSON from '../../package.json' with { type: "json" };
|
import packageJSON from '../../package.json' with { type: 'json' };
|
||||||
const version = packageJSON.version;
|
|
||||||
|
|
||||||
export default (req, res, next)=>{
|
export default (req, res, next)=>{
|
||||||
const userVersion = req.get('Homebrewery-Version');
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = packageJSON.version;
|
||||||
|
|
||||||
if(userVersion != version) {
|
if(userVersion !== version) {
|
||||||
return res.status(412).send({
|
return res.status(412).send({
|
||||||
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||||
});
|
});
|
||||||
@@ -12,3 +12,4 @@ export default (req, res, next)=>{
|
|||||||
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default (req, res, next)=>{
|
|||||||
const isImageRequest = req.get('Accept')?.split(',')
|
const isImageRequest = req.get('Accept')?.split(',')
|
||||||
?.filter((h)=>!h.includes('q='))
|
?.filter((h)=>!h.includes('q='))
|
||||||
?.every((h)=>/image\/.*/.test(h));
|
?.every((h)=>/image\/.*/.test(h));
|
||||||
if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
|
if(isImageRequest && !(isLocalEnvironment && req.url?.startsWith('/staticImages'))) {
|
||||||
return res.status(406).send({
|
return res.status(406).send({
|
||||||
message : 'Request for image at this URL is not supported'
|
message : 'Request for image at this URL is not supported'
|
||||||
});
|
});
|
||||||
|
|||||||
41
server/middleware/content-negotiation.spec.js
Normal file
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import contentNegotiationMiddleware from './content-negotiation.js';
|
||||||
|
|
||||||
|
describe('content-negotiation-middleware', ()=>{
|
||||||
|
let request;
|
||||||
|
let response;
|
||||||
|
let next;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
request = {
|
||||||
|
get : function(key) {
|
||||||
|
return this[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
response = {
|
||||||
|
status : jest.fn(()=>response),
|
||||||
|
send : jest.fn(()=>{})
|
||||||
|
};
|
||||||
|
next = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 406 on image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response);
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenLastCalledWith(406);
|
||||||
|
expect(response.send).toHaveBeenCalledWith({
|
||||||
|
message : 'Request for image at this URL is not supported'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next on non-image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,49 +11,54 @@
|
|||||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
@keyframes sourceMoveAnimation {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% { color : white;background-color : red;}
|
||||||
100% {background-color: unset; color: unset;}
|
100% { color : unset;background-color : unset;}
|
||||||
}
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor {
|
||||||
@media screen and (pointer : coarse) {
|
@media screen and (pointer : coarse) {
|
||||||
font-size : 16px;
|
font-size : 16px;
|
||||||
}
|
}
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family : inherit;
|
||||||
text-shadow: none;
|
font-weight : 600;
|
||||||
font-weight: 600;
|
color : grey;
|
||||||
color: grey;
|
text-shadow : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceMoveFlash .CodeMirror-line{
|
.CodeMirror-foldgutter {
|
||||||
animation-name: sourceMoveAnimation;
|
cursor : pointer;
|
||||||
animation-duration: 0.4s;
|
border-left : 1px solid #EEEEEE;
|
||||||
}
|
transition : background 0.1s;
|
||||||
|
&:hover { background : #DDDDDD; }
|
||||||
|
}
|
||||||
|
|
||||||
.CodeMirror-vscrollbar {
|
.sourceMoveFlash .CodeMirror-line {
|
||||||
&::-webkit-scrollbar {
|
animation-name : sourceMoveAnimation;
|
||||||
width: 20px;
|
animation-duration : 0.4s;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
width: 20px;
|
.CodeMirror-vscrollbar {
|
||||||
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
&::-webkit-scrollbar { width : 20px; }
|
||||||
}
|
&::-webkit-scrollbar-thumb {
|
||||||
}
|
width : 20px;
|
||||||
|
background : linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//.cm-tab {
|
//.cm-tab {
|
||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
//.cm-trailingspace {
|
//.cm-trailingspace {
|
||||||
// .cm-space {
|
// .cm-space {
|
||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emojiPreview {
|
.emojiPreview {
|
||||||
font-size: 1.5em;
|
font-size : 1.5em;
|
||||||
line-height: 1.2em;
|
line-height : 1.2em;
|
||||||
}
|
}
|
||||||
@@ -391,10 +391,31 @@ 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 = {
|
const definitionListsSingleLine = {
|
||||||
name : 'definitionListsSingleLine',
|
name : 'definitionListsSingleLine',
|
||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||||
let match;
|
let match;
|
||||||
@@ -748,11 +769,12 @@ const tableTerminators = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
|
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks,
|
||||||
mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
nonbreakingSpaces, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
|
||||||
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }),
|
||||||
|
MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
|
||||||
|
|
||||||
function cleanUrl(href) {
|
function cleanUrl(href) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -43,5 +43,6 @@ html,body, #reactRoot{
|
|||||||
}
|
}
|
||||||
&:disabled{
|
&:disabled{
|
||||||
background-color : @silver !important;
|
background-color : @silver !important;
|
||||||
|
cursor:not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
72
tests/markdown/non-breaking-spaces.test.js
Normal file
72
tests/markdown/non-breaking-spaces.test.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
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(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Double Space', function() {
|
||||||
|
const source = ':>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Triple Space', function() {
|
||||||
|
const source = ':>>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Many Space', function() {
|
||||||
|
const source = ':>>>>>>>>>>\n\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple sets of Spaces', function() {
|
||||||
|
const source = ':>>>\n:>>>\n:>>>';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> \n \n </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Pair of inline Spaces', function() {
|
||||||
|
const source = ':>>:>>';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<p> </p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(`<p>Line 1\n \nLine 2</p>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(`<pre><code>\n:>\n</code></pre>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a single-line definition list!', function() {
|
||||||
|
const source = 'Term ::> Definition 1\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt><dd>> Definition 1</dd>\n</dl>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a definition list!', function() {
|
||||||
|
const source = 'Term\n::> Definition 1\n';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd></dl>`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('I am actually a two-term definition list!', function() {
|
||||||
|
const source = 'Term\n::> Definition 1\n::>> Definition 2';
|
||||||
|
const rendered = Markdown.render(source).trim();
|
||||||
|
expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`<dl><dt>Term</dt>\n<dd>> Definition 1</dd>\n<dd>>> Definition 2</dd></dl>`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user