0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-08 22:32:41 +00:00

Merge branch 'master' into document-tags

# Conflicts:
#	client/homebrew/editor/metadataEditor/metadataEditor.jsx
#	server/homebrew.api.js
This commit is contained in:
Charlie Humphreys
2022-06-24 22:55:35 -05:00
85 changed files with 10786 additions and 4300 deletions

View File

@@ -26,6 +26,7 @@ const splice = function(str, index, inject){
const Editor = createClass({
displayName : 'Editor',
getDefaultProps : function() {
return {
brew : {
@@ -60,8 +61,14 @@ const Editor = createClass({
window.removeEventListener('resize', this.updateEditorSize);
},
componentDidUpdate : function() {
componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew) {
this.brewJump();
};
if(prevProps.moveSource !== this.props.moveSource) {
this.sourceJump();
};
},
updateEditorSize : function() {
@@ -89,15 +96,20 @@ const Editor = createClass({
},
handleViewChange : function(newView){
this.props.setMoveArrows(newView === 'text');
this.setState({
view : newView
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
},
getCurrentPage : function(){
const lines = this.props.brew.text.split('\n').slice(0, this.cursorPosition.line + 1);
const lines = this.props.brew.text.split('\n').slice(0, this.refs.codeEditor.getCursorPosition().line + 1);
return _.reduce(lines, (r, line)=>{
if(line.indexOf('\\page') !== -1) r++;
if(
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
||
(this.props.renderer == 'V3' && line.match(/^\\page$/))
) r++;
return r;
}, 1);
},
@@ -107,73 +119,143 @@ const Editor = createClass({
if(this.state.view === 'text') {
const codeMirror = this.refs.codeEditor.codeMirror;
//reset custom text styles
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
codeMirror.operation(()=>{ // Batch CodeMirror styling
//reset custom text styles
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
let editorPageCount = 2; // start page count from page 2
//reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background');
codeMirror.removeLineClass(lineNumber, 'text');
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
// Legacy Codemirror styling
if(this.props.renderer == 'legacy') {
if(line.includes('\\page')){
//reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
codeMirror.removeLineClass(lineNumber, 'text');
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
// Styling for \page breaks
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
// add back the original class 'background' but also add the new class '.pageline'
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
}
const pageCountElement = Object.assign(document.createElement('span'), {
className : 'editor-page-count',
textContent : editorPageCount
});
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(line.match(/^\\page$/)){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
editorPageCount += 1;
};
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
r.push(lineNumber);
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
}
}
return r;
}, []);
return lineNumbers;
});
});
}
},
brewJump : function(){
const currentPage = this.getCurrentPage();
window.location.hash = `p${currentPage}`;
brewJump : function(targetPage=this.getCurrentPage()){
if(!window) return;
// console.log(`Scroll to: p${targetPage}`);
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
const interimPos = targetPos >= 0 ? -30 : 30;
const bounceDelay = 100;
const scrollDelay = 500;
if(!this.throttleBrewMove) {
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
setTimeout(()=>{
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay);
}, scrollDelay, { leading: true, trailing: false });
};
this.throttleBrewMove(currentPos, interimPos, targetPos);
// const hashPage = (page != 1) ? `p${page}` : '';
// window.location.hash = hashPage;
},
sourceJump : function(targetLine=null){
if(this.isText()) {
if(targetLine == null) {
targetLine = 0;
const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page');
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height;
let currentPage = 1;
for (const page of pageCollection) {
if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) {
currentPage = parseInt(page.id.slice(1)) || 1;
break;
}
}
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-1).join(textSplit);
const textPosition = textString.length;
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
let currentY = this.refs.codeEditor.codeMirror.getScrollInfo().top;
let targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.refs.codeEditor.codeMirror.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.refs.codeEditor.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.refs.codeEditor.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.refs.codeEditor.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
}
}, 10);
}
}
},
//Called when there are changes to the editor's dimensions
@@ -206,6 +288,7 @@ const Editor = createClass({
view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange}
enableFolding={false}
rerenderParent={this.rerenderParent} />
</>;
}

View File

@@ -5,17 +5,13 @@
.codeEditor{
height : 100%;
counter-reset : page;
counter-increment : page;
.pageLine{
background : #33333328;
border-top : #339 solid 1px;
&:after{
content : counter(page);
counter-increment : page;
float : right;
color : gray;
}
}
.editor-page-count{
color : grey;
float : right;
}
.columnSplit{
font-style : italic;

View File

@@ -8,7 +8,10 @@ const request = require('superagent');
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
const homebreweryThumbnail = require('../../thumbnail.png');
const MetadataEditor = createClass({
displayName : 'MetadataEditor',
getDefaultProps : function() {
return {
metadata : {
@@ -57,6 +60,23 @@ const MetadataEditor = createClass({
}
},
getInitialState : function(){
return {
showThumbnail : true
};
},
toggleThumbnailDisplay : function(){
this.setState({
showThumbnail : !this.state.showThumbnail
});
},
renderThumbnail : function(){
if(!this.state.showThumbnail) return;
return <img className='thumbnail-preview' src={this.props.metadata.thumbnail || homebreweryThumbnail}></img>;
},
handleFieldChange : function(name, e){
this.props.onChange(_.merge({}, this.props.metadata, {
[name] : e.target.value
@@ -91,7 +111,7 @@ const MetadataEditor = createClass({
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
}
request.delete(`/api/${this.props.metadata.editId}`)
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
.send()
.end(function(err, res){
window.location.href = '/';
@@ -256,6 +276,18 @@ const MetadataEditor = createClass({
<textarea value={this.props.metadata.description} className='value'
onChange={(e)=>this.handleFieldChange('description', e)} />
</div>
<div className='field thumbnail'>
<label>thumbnail</label>
<input type='text'
value={this.props.metadata.thumbnail}
placeholder='my.thumbnail.url'
className='value'
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
<button className='display' onClick={this.toggleThumbnailDisplay}>
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
</button>
{this.renderThumbnail()}
</div>
{this.renderTags()}

View File

@@ -24,6 +24,33 @@
flex : 1 1 auto;
min-width : 200px;
}
&.thumbnail{
height : 1.4em;
label{
line-height: 2.0em;
}
.value{
overflow: hidden;
text-overflow: ellipsis;
}
button{
border: 1px solid #999;
color: white;
padding: 0px 5px;
background-color: black;
&:hover{
background-color: #777;
}
}
.thumbnail-preview{
position : relative;
width : 80px;
height : min-content;
border : 2px solid white;
margin-left : 5px;
max-height : 115px;
}
}
}
.description.field textarea.value{
resize : none;

View File

@@ -14,6 +14,7 @@ const execute = function(val, brew){
};
const Snippetbar = createClass({
displayName : 'SnippetBar',
getDefaultProps : function() {
return {
brew : {},
@@ -103,6 +104,7 @@ module.exports = Snippetbar;
const SnippetGroup = createClass({
displayName : 'SnippetGroup',
getDefaultProps : function() {
return {
brew : {},

View File

@@ -9,6 +9,7 @@ module.exports = function(classname){
classname = classname.toLowerCase();
const hitDie = _.sample([4, 6, 8, 10, 12]);
const spellSkill = _.sample(['Wisdom', 'Charisma', 'Intelligence']);
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
const skillList = ['Acrobatics', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
@@ -27,11 +28,19 @@ module.exports = function(classname){
**Armor:** :: ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}
**Weapons:** :: ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}
**Tools:** :: ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}
**Tools:** :: ${_.sampleSize(['Artisan\'s tools', 'one musical instrument', 'Thieves\' tools'], _.random(0, 2)).join(', ') || 'None'}
**Saving Throws:** :: ${_.sampleSize(abilityList, 2).join(', ')}
**Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}
#### Spellcasting Ability
{{text-align:center
**Spell save DC**:: = ${_.sample([6, 8, 10])} + your proficiency bonus + your ${spellSkill} modifier
**Spell attack modifier**:: = your proficiency bonus + your ${spellSkill} modifier
}}
#### Equipment
You start with the following equipment, in addition to the equipment granted by your background:
- *(a)* a martial weapon and a shield or *(b)* two martial weapons

View File

@@ -100,25 +100,25 @@ const subtitles = [
module.exports = ()=>{
return `<style>
.phb#p1{ text-align:center; }
.phb#p1:after{ display:none; }
.phb#p2 { counter-reset:phb-page-numbers; }
.phb:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
.phb:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
.phb:nth-child(2n)::after { transform: scaleX(1); }
.phb:nth-child(2n+1)::after { transform: scaleX(-1); }
.phb:nth-child(2n) .footnote { left: inherit; text-align: right; }
.phb:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
.page#p1{ text-align:center; counter-increment: none; }
.page#p1:after{ display:none; }
.page:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
.page:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
.page:nth-child(2n)::after { transform: scaleX(1); }
.page:nth-child(2n+1)::after { transform: scaleX(-1); }
.page:nth-child(2n) .footnote { left: inherit; text-align: right; }
.page:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
</style>
<div style='margin-top:450px;'></div>
{{margin-top:225px}}
# ${_.sample(titles)}
<div style='margin-top:25px'></div>
<div class='wide'>
{{margin-top:25px}}
{{wide
##### ${_.sample(subtitles)}
</div>
}}
\\page`;
};

View File

@@ -97,7 +97,11 @@ module.exports = [
gen : dedent`/* Removes Drop Caps */
.page h1+p:first-letter {
all: unset;
}\n\n`
}\n\n
/* Removes Small-Caps in first line */
.page h1+p:first-line {
all: unset;
}`
},
{
name : 'Tweak Drop Cap',

View File

@@ -8,6 +8,7 @@ module.exports = function(classname){
classname = classname.toLowerCase();
const hitDie = _.sample([4, 6, 8, 10, 12]);
const spellSkill = _.sample(['Wisdom', 'Charisma', 'Intelligence']);
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
@@ -26,12 +27,21 @@ module.exports = function(classname){
'___',
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
`- **Tools:** ${_.sampleSize(['Artisan\'s tools', 'one musical instrument', 'Thieves\' tools'], _.random(0, 2)).join(', ') || 'None'}`,
'',
'___',
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
'',
'#### Spellcasting Ability',
'',
`<div style=text-align:center>`,
'___',
`- **Spell save DC** = ${_.sample([6, 8, 10])} + your proficiency bonus + your ${spellSkill} modifier`,
'',
`- **Spell attack modifier** = your proficiency bonus + your ${spellSkill} modifier`,
`</div>`,
'',
'#### Equipment',
'You start with the following equipment, in addition to the equipment granted by your background:',
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',