From c88d9b55d7d5f7fe9eccfafba0631ed802b88f4a Mon Sep 17 00:00:00 2001 From: Frank Steinberg <steinberg@ibr.cs.tu-bs.de> Date: Sun, 6 Oct 2019 22:06:03 +0200 Subject: [PATCH] First steps towards a web online translation editor. --- Makefile | 7 +- web/bjcp-styleguide.css | 158 +++++++++++++++++++++ web/edit.css | 43 ++++++ web/edit.js | 64 +++++++++ web/pell.css | 27 ++++ web/pell.js | 224 ++++++++++++++++++++++++++++++ xsl/bjcp-2015-styleguide-html.xsl | 217 ++++++++--------------------- 7 files changed, 579 insertions(+), 161 deletions(-) create mode 100644 web/bjcp-styleguide.css create mode 100644 web/edit.css create mode 100644 web/edit.js create mode 100644 web/pell.css create mode 100644 web/pell.js diff --git a/Makefile b/Makefile index f2f2388..b2b6a64 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ DEFILES = $(shell ls de/*.xml) FIXFILES = $(shell ls fix/*.xml) -default: bjcp-2015-styleguide-orig.xml bjcp-2015-styleguide-de.xml bjcp-2015-styleguide-orig.html bjcp-2015-styleguide-de.html +default: bjcp-2015-styleguide-orig.xml bjcp-2015-styleguide-de.xml bjcp-2015-styleguide-orig.html bjcp-2015-styleguide-de.html bjcp-2015-styleguide-de-edit.html cache/2015_Guidelines_Beer.docx: @if [ ! -d cache ] ; then mkdir cache ; fi @@ -32,6 +32,10 @@ bjcp-2015-styleguide-de.html: xsl/bjcp-2015-styleguide-html.xsl bjcp-2015-styleg @xsltproc xsl/bjcp-2015-styleguide-html.xsl bjcp-2015-styleguide-de.xml > bjcp-2015-styleguide-de.html @echo "built $@" +bjcp-2015-styleguide-de-edit.html: xsl/bjcp-2015-styleguide-html.xsl bjcp-2015-styleguide-de.xml + @xsltproc --stringparam edit yes xsl/bjcp-2015-styleguide-html.xsl bjcp-2015-styleguide-de.xml > bjcp-2015-styleguide-de-edit.html + @echo "built $@" + format: @for f in de/*.xml ; do xmllint --format $$f | sed -e 's/ standalone="yes"//' > cache/tmp.xml ; cmp -s cache/tmp.xml $$f ; if [ $$? -ne 0 ] ; then cat cache/tmp.xml > $$f ; echo "reformatted $$f" ; fi ; rm cache/tmp.xml ; done @@ -50,6 +54,7 @@ clean: @rm -f bjcp-2015-styleguide-de.xml @rm -f bjcp-2015-styleguide-orig.html @rm -f bjcp-2015-styleguide-de.html + @rm -f bjcp-2015-styleguide-de-edit.html @echo "cleanup done" distclean: clean diff --git a/web/bjcp-styleguide.css b/web/bjcp-styleguide.css new file mode 100644 index 0000000..cef49fb --- /dev/null +++ b/web/bjcp-styleguide.css @@ -0,0 +1,158 @@ + +styleguide { + font-family: Helvetica, Arial, Geneva, sans-serif; + font-size: 8pt; +} + +styleguide chapter table { + font-size: 8pt; +} + +styleguide p.list-item { + list-style-type: decimal; +} + +styleguide category, styleguide subcategory { + font-size: 9pt; + margin-top:1em; + margin-bottom:1em; + margin-left:0em; + display: list-item; + list-style-position: inside; +} + +styleguide p { + margin-top:1em; +} + +styleguide b { + font-weight: bold; +} + +styleguide i { + font-style: italic; +} + +styleguide u { + text-decoration: underline; +} + +styleguide category:before, styleguide subcategory:before { + content: attr(id); +} +styleguide name { + display: inline-block; + margin-bottom:1em; +} +styleguide description { + font-size: 8pt; + margin-left:1em; + display: block; +} +styleguide description p { + margin-top: 0; +} + +styleguide overall-impression:before { + content: "Overall Impression: "; +} +styleguide aroma:before { + content: "Aroma: "; +} +styleguide appearance:before { + content: "Appearance: "; +} +styleguide flavor:before { + content: "Flavor: "; +} +styleguide mouthfeel:before { + content: "Mouthfeel: "; +} +styleguide comments:before { + content: "Comments: "; +} +styleguide history:before { + content: "History: "; +} +styleguide characteristic-ingredients:before { + content: "Characteristic Ingredients: "; +} +styleguide style-comparison:before { + content: "Style Comparison: "; +} +styleguide entry-instructions:before { + content: "Entry Instructions: "; +} +styleguide specs:before { + content: "Vital Statistics: "; +} +styleguide commercial-examples:before { + content: "Commercial Examples: "; +} +styleguide tags:before { + content: "Tags: "; +} +styleguide strength-classifications:before { + content: "Strength Classifications: "; +} + +styleguide overall-impression, +styleguide aroma, +styleguide appearance, +styleguide flavor, +styleguide mouthfeel, +styleguide comments, +styleguide history, +styleguide characteristic-ingredients, +styleguide style-comparison, +styleguide specs, +styleguide entry-instructions, +styleguide commercial-examples, +styleguide tags, +styleguide strength-classifications { + position: relative; + margin-left: 1em; + display: block; + font-size: 8pt; +} +styleguide overall-impression:before, +styleguide aroma:before, +styleguide appearance:before, +styleguide flavor:before, +styleguide mouthfeel:before, +styleguide comments:before, +styleguide history:before, +styleguide characteristic-ingredients:before, +styleguide style-comparison:before, +styleguide specs:before, +styleguide entry-instructions:before, +styleguide commercial-examples:before, +styleguide tags:before, +styleguide strength-classifications:before { + font-weight: bold; +} + +styleguide specs div.ibu:before { + content: "IBU: "; +} +styleguide specs div.srm:before { + content: "SRM: "; +} +styleguide specs div.og:before { + content: "OG: "; +} +styleguide specs div.fg:before { + content: "FG: "; +} +styleguide specs div.abv:before { + content: "ABV: "; +} +styleguide specs div { + display: inline-block; +} +styleguide specs div * { + border: 1px solid; + display: inline-block; + width: 9em; +} + diff --git a/web/edit.css b/web/edit.css new file mode 100644 index 0000000..5d9a0af --- /dev/null +++ b/web/edit.css @@ -0,0 +1,43 @@ + +.pell-content{ + border-bottom: 1px solid black; +} + +div#editor button[title="save"], +div#editor button[title="cancel"] { + width: 70px; +} +div#editor { + display: none; + position: fixed; + width: 100%; + top: 0; + left: 0; + right: 0; + text-align: center; + font-family: Helvetica, Arial, Geneva, sans-serif; + font-size: 8pt; +} +div#editor-inner { + text-align: left; + margin: 50 auto; + padding: 5px; + width: 80%; + z-index: 2; + border: 2px solid black; + background: #ddd; +} +div#editor-inner input { + background: white; +} +div#markup { + font-family: courier; + font-size: 7pt; +} +div#render { + font-size: 7pt; +} + +*[onclick] { + cursor: pointer; +} diff --git a/web/edit.js b/web/edit.js new file mode 100644 index 0000000..5ad2141 --- /dev/null +++ b/web/edit.js @@ -0,0 +1,64 @@ + +const editor = document.getElementById("editor"); +const pell = window.pell; +const pelleditor = document.getElementById("pelleditor"); +const markup = document.getElementById("markup"); + +pell.init({ + element: pelleditor, + actions: [ + 'bold', + 'italic', + 'underline', + { + name: 'link', + result: () => { + const idref = window.prompt('Enter the target category ID'); + if (idref) { + var selection = document.getSelection(); + if (selection == "") { + text = window.prompt('Enter link text (optional)'); + pell.exec('insertHTML', '<a href="#' + idref + '">' + text + '</a>'); + } else { + //pell.exec('insertHTML', '<a idref="#' + idref + '">' + text + '</a>'); + pell.exec('createLink', "#" + idref); + } + } + } + }, + { + name: 'save', + icon: '<div style="background-color:pink;">save</div>', + title: 'save', + result: () => { + editor.style.display = "none"; + } + }, + { + name: 'cancel', + icon: '<div style="background-color:pink;">cancel</div>', + title: 'cancel', + result: () => { + editor.style.display = "none"; + } + }, + ], + onChange: (html) => { + x = html; + x = x.replace(/ /g, " "); + x = x.replace(/<div>/g, " "); + x = x.replace(/<\/div>/g, " "); + x = x.replace(/<br>/g, " "); + x = x.replace(/ */g, " "); + markup.innerText = x; + render.innerHTML = x; + } +}) + +function doedit(elem) { + pelleditor.content.innerHTML = elem.innerHTML; + editor.style.display = "block"; + markup.innerText = elem.innerHTML; + render.innerHTML = elem.innerHTML; +} + diff --git a/web/pell.css b/web/pell.css new file mode 100644 index 0000000..5d6916b --- /dev/null +++ b/web/pell.css @@ -0,0 +1,27 @@ +.pell { + border: 1px solid rgba(10, 10, 10, 0.1); + box-sizing: border-box; } + +.pell-content { + background: white; + box-sizing: border-box; + /*height: 200px;*/ + outline: 0; + /*overflow-y: auto;*/ + padding: 10px; } + +.pell-actionbar { + background-color: #ddd; + border-bottom: 1px solid rgba(10, 10, 10, 0.1); } + +.pell-button { + background-color: transparent; + border: none; + cursor: pointer; + height: 30px; + outline: 0; + width: 30px; + vertical-align: bottom; } + +.pell-button-selected { + background-color: #F0F0F0; } diff --git a/web/pell.js b/web/pell.js new file mode 100644 index 0000000..36e2c22 --- /dev/null +++ b/web/pell.js @@ -0,0 +1,224 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.pell = {}))); +}(this, (function (exports) { 'use strict'; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var defaultParagraphSeparatorString = 'defaultParagraphSeparator'; +var formatBlock = 'formatBlock'; +var addEventListener = function addEventListener(parent, type, listener) { + return parent.addEventListener(type, listener); +}; +var appendChild = function appendChild(parent, child) { + return parent.appendChild(child); +}; +var createElement = function createElement(tag) { + return document.createElement(tag); +}; +var queryCommandState = function queryCommandState(command) { + return document.queryCommandState(command); +}; +var queryCommandValue = function queryCommandValue(command) { + return document.queryCommandValue(command); +}; + +var exec = function exec(command) { + var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + return document.execCommand(command, false, value); +}; + +var defaultActions = { + bold: { + icon: '<b>B</b>', + title: 'Bold', + state: function state() { + return queryCommandState('bold'); + }, + result: function result() { + return exec('bold'); + } + }, + italic: { + icon: '<i>I</i>', + title: 'Italic', + state: function state() { + return queryCommandState('italic'); + }, + result: function result() { + return exec('italic'); + } + }, + underline: { + icon: '<u>U</u>', + title: 'Underline', + state: function state() { + return queryCommandState('underline'); + }, + result: function result() { + return exec('underline'); + } + }, + strikethrough: { + icon: '<strike>S</strike>', + title: 'Strike-through', + state: function state() { + return queryCommandState('strikeThrough'); + }, + result: function result() { + return exec('strikeThrough'); + } + }, + heading1: { + icon: '<b>H<sub>1</sub></b>', + title: 'Heading 1', + result: function result() { + return exec(formatBlock, '<h1>'); + } + }, + heading2: { + icon: '<b>H<sub>2</sub></b>', + title: 'Heading 2', + result: function result() { + return exec(formatBlock, '<h2>'); + } + }, + paragraph: { + icon: '¶', + title: 'Paragraph', + result: function result() { + return exec(formatBlock, '<p>'); + } + }, + quote: { + icon: '“ ”', + title: 'Quote', + result: function result() { + return exec(formatBlock, '<blockquote>'); + } + }, + olist: { + icon: '#', + title: 'Ordered List', + result: function result() { + return exec('insertOrderedList'); + } + }, + ulist: { + icon: '•', + title: 'Unordered List', + result: function result() { + return exec('insertUnorderedList'); + } + }, + code: { + icon: '</>', + title: 'Code', + result: function result() { + return exec(formatBlock, '<pre>'); + } + }, + line: { + icon: '―', + title: 'Horizontal Line', + result: function result() { + return exec('insertHorizontalRule'); + } + }, + link: { + icon: '🔗', + title: 'Link', + result: function result() { + var url = window.prompt('Enter the link URL'); + if (url) exec('createLink', url); + } + }, + image: { + icon: '📷', + title: 'Image', + result: function result() { + var url = window.prompt('Enter the image URL'); + if (url) exec('insertImage', url); + } + } +}; + +var defaultClasses = { + actionbar: 'pell-actionbar', + button: 'pell-button', + content: 'pell-content', + selected: 'pell-button-selected' +}; + +var init = function init(settings) { + var actions = settings.actions ? settings.actions.map(function (action) { + if (typeof action === 'string') return defaultActions[action];else if (defaultActions[action.name]) return _extends({}, defaultActions[action.name], action); + return action; + }) : Object.keys(defaultActions).map(function (action) { + return defaultActions[action]; + }); + + var classes = _extends({}, defaultClasses, settings.classes); + + var defaultParagraphSeparator = settings[defaultParagraphSeparatorString] || 'div'; + + var actionbar = createElement('div'); + actionbar.className = classes.actionbar; + appendChild(settings.element, actionbar); + + var content = settings.element.content = createElement('div'); + content.contentEditable = true; + content.className = classes.content; + content.oninput = function (_ref) { + var firstChild = _ref.target.firstChild; + + if (firstChild && firstChild.nodeType === 3) exec(formatBlock, '<' + defaultParagraphSeparator + '>');else if (content.innerHTML === '<br>') content.innerHTML = ''; + settings.onChange(content.innerHTML); + }; + content.onkeydown = function (event) { + if (event.key === 'Enter' && queryCommandValue(formatBlock) === 'blockquote') { + setTimeout(function () { + return exec(formatBlock, '<' + defaultParagraphSeparator + '>'); + }, 0); + } + }; + appendChild(settings.element, content); + + actions.forEach(function (action) { + var button = createElement('button'); + button.className = classes.button; + button.innerHTML = action.icon; + button.title = action.title; + button.setAttribute('type', 'button'); + button.onclick = function () { + return action.result() && content.focus(); + }; + + if (action.state) { + var handler = function handler() { + return button.classList[action.state() ? 'add' : 'remove'](classes.selected); + }; + addEventListener(content, 'keyup', handler); + addEventListener(content, 'mouseup', handler); + addEventListener(button, 'click', handler); + } + + appendChild(actionbar, button); + }); + + if (settings.styleWithCSS) exec('styleWithCSS'); + exec(defaultParagraphSeparatorString, defaultParagraphSeparator); + + return settings.element; +}; + +var pell = { exec: exec, init: init }; + +exports.exec = exec; +exports.init = init; +exports['default'] = pell; + +Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/xsl/bjcp-2015-styleguide-html.xsl b/xsl/bjcp-2015-styleguide-html.xsl index 2855688..12483af 100644 --- a/xsl/bjcp-2015-styleguide-html.xsl +++ b/xsl/bjcp-2015-styleguide-html.xsl @@ -7,6 +7,10 @@ + <xsl:param name="edit">no</xsl:param> + + + <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> @@ -17,169 +21,45 @@ <xsl:element name="title"> <xsl:text>BJCP 2015 Styleguide</xsl:text> </xsl:element> - <xsl:element name="style"> - -styleguide { - font-family: Helvetica, Arial, Geneva, sans-serif; - font-size: 8pt; -} - -styleguide chapter table { - font-size: 8pt; -} - -styleguide p.list-item { - list-style-type: decimal; -} - -styleguide category, styleguide subcategory { - font-size: 9pt; - margin-top:1em; - margin-bottom:1em; - margin-left:0em; - display: list-item; - list-style-position: inside; -} - -styleguide p { - margin-top:1em; -} - -styleguide b { - font-weight: bold; -} - -styleguide i { - font-style: italic; -} - -styleguide u { - text-decoration: underline; -} - -styleguide category:before, styleguide subcategory:before { - content: attr(id); -} -styleguide name { - display: inline-block; - margin-bottom:1em; -} -styleguide description { - font-size: 8pt; - margin-left:1em; - display: block; -} -styleguide description p { - margin-top: 0; -} - -styleguide overall-impression:before { - content: "Overall Impression: "; -} -styleguide aroma:before { - content: "Aroma: "; -} -styleguide appearance:before { - content: "Appearance: "; -} -styleguide flavor:before { - content: "Flavor: "; -} -styleguide mouthfeel:before { - content: "Mouthfeel: "; -} -styleguide comments:before { - content: "Comments: "; -} -styleguide history:before { - content: "History: "; -} -styleguide characteristic-ingredients:before { - content: "Characteristic Ingredients: "; -} -styleguide style-comparison:before { - content: "Style Comparison: "; -} -styleguide entry-instructions:before { - content: "Entry Instructions: "; -} -styleguide specs:before { - content: "Vital Statistics: "; -} -styleguide commercial-examples:before { - content: "Commercial Examples: "; -} -styleguide tags:before { - content: "Tags: "; -} -styleguide strength-classifications:before { - content: "Strength Classifications: "; -} - -styleguide overall-impression, -styleguide aroma, -styleguide appearance, -styleguide flavor, -styleguide mouthfeel, -styleguide comments, -styleguide history, -styleguide characteristic-ingredients, -styleguide style-comparison, -styleguide specs, -styleguide entry-instructions, -styleguide commercial-examples, -styleguide tags, -styleguide strength-classifications { - position: relative; - margin-left: 1em; - display: block; - font-size: 8pt; -} -styleguide overall-impression:before, -styleguide aroma:before, -styleguide appearance:before, -styleguide flavor:before, -styleguide mouthfeel:before, -styleguide comments:before, -styleguide history:before, -styleguide characteristic-ingredients:before, -styleguide style-comparison:before, -styleguide specs:before, -styleguide entry-instructions:before, -styleguide commercial-examples:before, -styleguide tags:before, -styleguide strength-classifications:before { - font-weight: bold; -} - -styleguide specs div.ibu:before { - content: "IBU: "; -} -styleguide specs div.srm:before { - content: "SRM: "; -} -styleguide specs div.og:before { - content: "OG: "; -} -styleguide specs div.fg:before { - content: "FG: "; -} -styleguide specs div.abv:before { - content: "ABV: "; -} -styleguide specs div { - display: inline-block; -} -styleguide specs div * { - border: 1px solid; - display: inline-block; - width: 9em; -} - + <xsl:element name="link"> + <xsl:attribute name="rel">stylesheet</xsl:attribute> + <xsl:attribute name="type">text/css</xsl:attribute> + <xsl:attribute name="href">bjcp-styleguide.css</xsl:attribute> </xsl:element> + <xsl:element name="link"> + <xsl:attribute name="rel">stylesheet</xsl:attribute> + <xsl:attribute name="type">text/css</xsl:attribute> + <xsl:attribute name="href">pell.css</xsl:attribute> + </xsl:element> + <xsl:if test="not($edit = 'no')"> + <xsl:element name="link"> + <xsl:attribute name="rel">stylesheet</xsl:attribute> + <xsl:attribute name="type">text/css</xsl:attribute> + <xsl:attribute name="href">edit.css</xsl:attribute> + </xsl:element> + </xsl:if> </xsl:element> <xsl:element name="body"> <xsl:apply-templates select="." mode="copy"/> + <xsl:if test="not($edit = 'no')"> + <div id="editor"> + <div id="editor-inner"> + Your Author ID: <input type="text" name="author" id="author" /> + <div id="pelleditor"></div> + </div> + <div>Markup:<div id="markup"></div></div> + <div>Preview:<div id="render"></div></div> + </div> + <div/> + <xsl:element name="script"> + <xsl:attribute name="src">pell.js</xsl:attribute> + <xsl:text> </xsl:text> + </xsl:element> + <xsl:element name="script"> + <xsl:attribute name="src">edit.js</xsl:attribute> + <xsl:text> </xsl:text> + </xsl:element> + </xsl:if> </xsl:element> </xsl:element> </xsl:template> @@ -257,6 +137,9 @@ styleguide specs div * { <xsl:variable name="href"> <xsl:value-of select="@href"/> </xsl:variable> + <xsl:variable name="idref"> + <xsl:value-of select="translate(@href,'#','')"/> + </xsl:variable> <xsl:element name="{local-name(.)}"> <xsl:attribute name="href"> <xsl:value-of select="$href"/> @@ -266,7 +149,7 @@ styleguide specs div * { <xsl:value-of select="./text()"/> </xsl:when> <xsl:otherwise> - <xsl:value-of select="//bjcp:styleguide//*[@id=$href]/bjcp:name"/> + <xsl:value-of select="//bjcp:styleguide//bjcp:*[@id=$idref]/bjcp:name"/> </xsl:otherwise> </xsl:choose> </xsl:element> @@ -283,6 +166,20 @@ styleguide specs div * { + <xsl:template match="bjcp:name|bjcp:description|bjcp:overall-impression|bjcp:aroma|bjcp:appearance|bjcp:flavor|bjcp:mouthfeel|bjcp:comments|bjcp:history|bjcp:characteristic-ingredients|bjcp:style-comparison|bjcp:entry-instructions|bjcp:commercial-examples" mode="copy"> + <xsl:element name="{local-name(.)}"> + <xsl:apply-templates select="@*" mode="copy"/> + <xsl:if test="not($edit = 'no')"> + <xsl:attribute name="onclick"> + <xsl:text>doedit(this);</xsl:text> + </xsl:attribute> + </xsl:if> + <xsl:apply-templates mode="copy"/> + </xsl:element> + </xsl:template> + + + <xsl:template match="@id" mode="copy"> <xsl:attribute name="id"> <xsl:choose> -- GitLab