Merge pull request #350 from siikamiika/query-parser-html-templates

query parser html templates
This commit is contained in:
siikamiika 2020-02-11 11:07:05 +02:00 committed by GitHub
commit 9ffd0cb441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 178 additions and 160 deletions

View File

@ -27,7 +27,6 @@ Yomichan provides advanced features not available in other browser-based diction
* [Flashcard Creation](https://foosoft.net/projects/yomichan/#flashcard-creation) * [Flashcard Creation](https://foosoft.net/projects/yomichan/#flashcard-creation)
* [Keyboard Shortcuts](https://foosoft.net/projects/yomichan/#keyboard-shortcuts) * [Keyboard Shortcuts](https://foosoft.net/projects/yomichan/#keyboard-shortcuts)
* [Development](https://foosoft.net/projects/yomichan/#development) * [Development](https://foosoft.net/projects/yomichan/#development)
* [Templates](https://foosoft.net/projects/yomichan/#templates)
* [Dependencies](https://foosoft.net/projects/yomichan/#dependencies) * [Dependencies](https://foosoft.net/projects/yomichan/#dependencies)
* [Frequently Asked Questions](https://foosoft.net/projects/yomichan/#frequently-asked-questions) * [Frequently Asked Questions](https://foosoft.net/projects/yomichan/#frequently-asked-questions)
* [Screenshots](https://foosoft.net/projects/yomichan/#screenshots) * [Screenshots](https://foosoft.net/projects/yomichan/#screenshots)
@ -241,15 +240,6 @@ following basic guidelines when creating pull requests:
* Large pull requests without a clear scope will not be merged. * Large pull requests without a clear scope will not be merged.
* Incomplete or non-standalone features will not be merged. * Incomplete or non-standalone features will not be merged.
### Templates ###
Yomichan uses [Handlebars](https://handlebarsjs.com/) templates for user interface generation. The source templates are
found in the `tmpl` directory and the compiled version is stored in the `ext/bg/js/templates.js` file. If you modify the
source templates, you will need to also recompile them. If you are developing on Linux or Mac OS X, you can use the
included `build_tmpl.sh` and `build_tmpl_auto.sh` shell scripts to do this for you
([inotify-tools](https://github.com/rvoicilas/inotify-tools/wiki) required). Otherwise, simply execute `handlebars
tmpl/*.html -f ext/bg/js/templates.js` from the project's base directory to compile all the templates.
### Dependencies ### ### Dependencies ###
Yomichan uses several third-party libraries to function. Below are links to homepages, snapshots, and licenses of the exact Yomichan uses several third-party libraries to function. Below are links to homepages, snapshots, and licenses of the exact

View File

@ -1,2 +0,0 @@
#!/bin/sh
handlebars tmpl/*.html -f ext/bg/js/templates.js

View File

@ -1,16 +0,0 @@
#!/bin/bash
DIRECTORY_TO_OBSERVE="tmpl"
BUILD_SCRIPT="build_tmpl.sh"
function block_for_change {
inotifywait -e modify,move,create,delete $DIRECTORY_TO_OBSERVE
}
function build {
bash $BUILD_SCRIPT
}
build
while block_for_change; do
build
done

View File

@ -37,7 +37,6 @@
<script src="/bg/js/options.js"></script> <script src="/bg/js/options.js"></script>
<script src="/bg/js/profile-conditions.js"></script> <script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/request.js"></script> <script src="/bg/js/request.js"></script>
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/translator.js"></script> <script src="/bg/js/translator.js"></script>
<script src="/bg/js/util.js"></script> <script src="/bg/js/util.js"></script>
<script src="/mixed/js/audio.js"></script> <script src="/mixed/js/audio.js"></script>

View File

@ -33,6 +33,10 @@ function apiClipboardGet() {
return _apiInvoke('clipboardGet'); return _apiInvoke('clipboardGet');
} }
function apiGetQueryParserTemplatesHtml() {
return _apiInvoke('getQueryParserTemplatesHtml');
}
function _apiInvoke(action, params={}) { function _apiInvoke(action, params={}) {
const data = {action, params}; const data = {action, params};
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@ -567,6 +567,11 @@ class Backend {
return await requestText(url, 'GET'); return await requestText(url, 'GET');
} }
async _onApiGetQueryParserTemplatesHtml() {
const url = chrome.runtime.getURL('/bg/query-parser-templates.html');
return await requestText(url, 'GET');
}
_onApiGetZoom(params, sender) { _onApiGetZoom(params, sender) {
if (!sender || !sender.tab) { if (!sender || !sender.tab) {
return Promise.reject(new Error('Invalid tab')); return Promise.reject(new Error('Invalid tab'));
@ -854,6 +859,7 @@ Backend._messageHandlers = new Map([
['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)], ['getEnvironmentInfo', (self, ...args) => self._onApiGetEnvironmentInfo(...args)],
['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)], ['clipboardGet', (self, ...args) => self._onApiClipboardGet(...args)],
['getDisplayTemplatesHtml', (self, ...args) => self._onApiGetDisplayTemplatesHtml(...args)], ['getDisplayTemplatesHtml', (self, ...args) => self._onApiGetDisplayTemplatesHtml(...args)],
['getQueryParserTemplatesHtml', (self, ...args) => self._onApiGetQueryParserTemplatesHtml(...args)],
['getZoom', (self, ...args) => self._onApiGetZoom(...args)] ['getZoom', (self, ...args) => self._onApiGetZoom(...args)]
]); ]);

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
class QueryParserGenerator {
constructor() {
this._templateHandler = null;
}
async prepare() {
const html = await apiGetQueryParserTemplatesHtml();
this._templateHandler = new TemplateHandler(html);
}
createParseResult(terms, preview=false) {
const fragment = document.createDocumentFragment();
for (const term of terms) {
const termContainer = this._templateHandler.instantiate(preview ? 'term-preview' : 'term');
for (const segment of term) {
if (!segment.text.trim()) { continue; }
if (!segment.reading || !segment.reading.trim()) {
termContainer.appendChild(this.createSegmentText(segment.text));
} else {
termContainer.appendChild(this.createSegment(segment));
}
}
fragment.appendChild(termContainer);
}
return fragment;
}
createSegment(segment) {
const segmentContainer = this._templateHandler.instantiate('segment');
const segmentTextContainer = segmentContainer.querySelector('.query-parser-segment-text');
const segmentReadingContainer = segmentContainer.querySelector('.query-parser-segment-reading');
segmentTextContainer.appendChild(this.createSegmentText(segment.text));
segmentReadingContainer.innerText = segment.reading;
return segmentContainer;
}
createSegmentText(text) {
const fragment = document.createDocumentFragment();
for (const chr of text) {
const charContainer = this._templateHandler.instantiate('char');
charContainer.innerText = chr;
fragment.appendChild(charContainer);
}
return fragment;
}
createParserSelect(parseResults, selectedParser) {
const selectContainer = this._templateHandler.instantiate('select');
for (const parseResult of parseResults) {
const optionContainer = this._templateHandler.instantiate('select-option');
optionContainer.value = parseResult.id;
optionContainer.innerText = parseResult.name;
optionContainer.defaultSelected = selectedParser === parseResult.id;
selectContainer.appendChild(optionContainer);
}
return selectContainer;
}
}

View File

@ -19,14 +19,20 @@
class QueryParser extends TextScanner { class QueryParser extends TextScanner {
constructor(search) { constructor(search) {
super(document.querySelector('#query-parser'), [], [], []); super(document.querySelector('#query-parser-content'), [], [], []);
this.search = search; this.search = search;
this.parseResults = []; this.parseResults = [];
this.selectedParser = null; this.selectedParser = null;
this.queryParser = document.querySelector('#query-parser'); this.queryParser = document.querySelector('#query-parser-content');
this.queryParserSelect = document.querySelector('#query-parser-select'); this.queryParserSelect = document.querySelector('#query-parser-select-container');
this.queryParserGenerator = new QueryParserGenerator();
}
async prepare() {
await this.queryParserGenerator.prepare();
} }
onError(error) { onError(error) {
@ -64,7 +70,7 @@ class QueryParser extends TextScanner {
const selectedParser = e.target.value; const selectedParser = e.target.value;
this.selectedParser = selectedParser; this.selectedParser = selectedParser;
apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext()); apiOptionsSet({parsing: {selectedParser}}, this.search.getOptionsContext());
this.renderParseResult(this.getParseResult()); this.renderParseResult();
} }
getMouseEventListeners() { getMouseEventListeners() {
@ -113,13 +119,13 @@ class QueryParser extends TextScanner {
async setText(text) { async setText(text) {
this.search.setSpinnerVisible(true); this.search.setSpinnerVisible(true);
await this.setPreview(text); this.setPreview(text);
this.parseResults = await this.parseText(text); this.parseResults = await this.parseText(text);
this.refreshSelectedParser(); this.refreshSelectedParser();
this.renderParserSelect(); this.renderParserSelect();
await this.renderParseResult(); this.renderParseResult();
this.search.setSpinnerVisible(false); this.search.setSpinnerVisible(false);
} }
@ -146,57 +152,29 @@ class QueryParser extends TextScanner {
return results; return results;
} }
async setPreview(text) { setPreview(text) {
const previewTerms = []; const previewTerms = [];
for (let i = 0, ii = text.length; i < ii; i += 2) { for (let i = 0, ii = text.length; i < ii; i += 2) {
const tempText = text.substring(i, i + 2); const tempText = text.substring(i, i + 2);
previewTerms.push([{text: tempText.split('')}]); previewTerms.push([{text: tempText}]);
} }
this.queryParser.innerHTML = await apiTemplateRender('query-parser.html', { this.queryParser.textContent = '';
terms: previewTerms, this.queryParser.appendChild(this.queryParserGenerator.createParseResult(previewTerms, true));
preview: true
});
} }
renderParserSelect() { renderParserSelect() {
this.queryParserSelect.innerHTML = ''; this.queryParserSelect.innerHTML = '';
if (this.parseResults.length > 1) { if (this.parseResults.length > 1) {
const select = document.createElement('select'); const select = this.queryParserGenerator.createParserSelect(this.parseResults, this.selectedParser);
select.classList.add('form-control');
for (const parseResult of this.parseResults) {
const option = document.createElement('option');
option.value = parseResult.id;
option.innerText = parseResult.name;
option.defaultSelected = this.selectedParser === parseResult.id;
select.appendChild(option);
}
select.addEventListener('change', this.onParserChange.bind(this)); select.addEventListener('change', this.onParserChange.bind(this));
this.queryParserSelect.appendChild(select); this.queryParserSelect.appendChild(select);
} }
} }
async renderParseResult() { renderParseResult() {
const parseResult = this.getParseResult(); const parseResult = this.getParseResult();
if (!parseResult) { this.queryParser.textContent = '';
this.queryParser.innerHTML = ''; if (!parseResult) { return; }
return; this.queryParser.appendChild(this.queryParserGenerator.createParseResult(parseResult.parsedText));
}
this.queryParser.innerHTML = await apiTemplateRender(
'query-parser.html',
{terms: QueryParser.processParseResultForDisplay(parseResult.parsedText)}
);
}
static processParseResultForDisplay(result) {
return result.map((term) => {
return term.filter((part) => part.text.trim()).map((part) => {
return {
text: part.text.split(''),
reading: part.reading,
raw: !part.reading || !part.reading.trim()
};
});
});
} }
} }

View File

@ -49,6 +49,8 @@ class DisplaySearch extends Display {
try { try {
await this.initialize(); await this.initialize();
await this.queryParser.prepare();
const {queryParams: {query='', mode=''}} = parseUrl(window.location.href); const {queryParams: {query='', mode=''}} = parseUrl(window.location.href);
if (this.search !== null) { if (this.search !== null) {

View File

@ -1,55 +0,0 @@
(function() {
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['query-parser.html'] = template({"1":function(container,depth0,helpers,partials,data) {
var stack1, alias1=depth0 != null ? depth0 : (container.nullContext || {});
return ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.preview : depth0),{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.program(4, data, 0),"data":data})) != null ? stack1 : "")
+ ((stack1 = helpers.each.call(alias1,depth0,{"name":"each","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "</span>";
},"2":function(container,depth0,helpers,partials,data) {
return "<span class=\"query-parser-term-preview\">";
},"4":function(container,depth0,helpers,partials,data) {
return "<span class=\"query-parser-term\">";
},"6":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = container.invokePartial(partials.part,depth0,{"name":"part","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
},"8":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.raw : depth0),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.program(12, data, 0),"data":data})) != null ? stack1 : "");
},"9":function(container,depth0,helpers,partials,data) {
var stack1;
return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "");
},"10":function(container,depth0,helpers,partials,data) {
return "<span class=\"query-parser-char\">"
+ container.escapeExpression(container.lambda(depth0, depth0))
+ "</span>";
},"12":function(container,depth0,helpers,partials,data) {
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {});
return "<ruby>"
+ ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.text : depth0),{"name":"each","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
+ "<rt>"
+ container.escapeExpression(((helper = (helper = helpers.reading || (depth0 != null ? depth0.reading : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"reading","hash":{},"data":data}) : helper)))
+ "</rt></ruby>";
},"14":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return ((stack1 = container.invokePartial(partials.term,depth0,{"name":"term","hash":{"preview":(depths[1] != null ? depths[1].preview : depths[1])},"data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "");
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data,blockParams,depths) {
var stack1;
return ((stack1 = helpers.each.call(depth0 != null ? depth0 : (container.nullContext || {}),(depth0 != null ? depth0.terms : depth0),{"name":"each","hash":{},"fn":container.program(14, data, 0, blockParams, depths),"inverse":container.noop,"data":data})) != null ? stack1 : "");
},"main_d": function(fn, props, container, depth0, data, blockParams, depths) {
var decorators = container.decorators;
fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(1, data, 0, blockParams, depths),"inverse":container.noop,"args":["term"],"data":data}) || fn;
fn = decorators.inline(fn,props,container,{"name":"inline","hash":{},"fn":container.program(8, data, 0, blockParams, depths),"inverse":container.noop,"args":["part"],"data":data}) || fn;
return fn;
}
,"useDecorators":true,"usePartial":true,"useData":true,"useDepths":true});
})();

View File

@ -0,0 +1,11 @@
<!DOCTYPE html><html><head></head><body>
<template id="term-template"><span class="query-parser-term" data-type="normal"></span></template>
<template id="term-preview-template"><span class="query-parser-term" data-type="preview"></span></template>
<template id="segment-template"><ruby class="query-parser-segment"><span class="query-parser-segment-text"></span><rt class="query-parser-segment-reading"></rt></ruby></template>
<template id="char-template"><span class="query-parser-char"></span></template>
<template id="select-template"><select class="query-parser-select form-control"></select></template>
<template id="select-option-template"><option class="query-parser-select-option"></option></template>
</body></html>

View File

@ -48,8 +48,8 @@
<div id="spinner" hidden><img src="/mixed/img/spinner.gif"></div> <div id="spinner" hidden><img src="/mixed/img/spinner.gif"></div>
<div class="scan-disable"> <div class="scan-disable">
<div id="query-parser-select" class="input-group"></div> <div id="query-parser-select-container" class="input-group"></div>
<div id="query-parser"></div> <div id="query-parser-content"></div>
</div> </div>
<hr> <hr>
@ -78,7 +78,6 @@
<script src="/bg/js/dictionary.js"></script> <script src="/bg/js/dictionary.js"></script>
<script src="/bg/js/handlebars.js"></script> <script src="/bg/js/handlebars.js"></script>
<script src="/bg/js/japanese.js"></script> <script src="/bg/js/japanese.js"></script>
<script src="/bg/js/templates.js"></script>
<script src="/fg/js/document.js"></script> <script src="/fg/js/document.js"></script>
<script src="/fg/js/source.js"></script> <script src="/fg/js/source.js"></script>
<script src="/mixed/js/audio.js"></script> <script src="/mixed/js/audio.js"></script>
@ -87,7 +86,9 @@
<script src="/mixed/js/display-generator.js"></script> <script src="/mixed/js/display-generator.js"></script>
<script src="/mixed/js/scroll.js"></script> <script src="/mixed/js/scroll.js"></script>
<script src="/mixed/js/text-scanner.js"></script> <script src="/mixed/js/text-scanner.js"></script>
<script src="/mixed/js/template-handler.js"></script>
<script src="/bg/js/search-query-parser-generator.js"></script>
<script src="/bg/js/search-query-parser.js"></script> <script src="/bg/js/search-query-parser.js"></script>
<script src="/bg/js/clipboard-monitor.js"></script> <script src="/bg/js/clipboard-monitor.js"></script>
<script src="/bg/js/search.js"></script> <script src="/bg/js/search.js"></script>

View File

@ -1097,7 +1097,6 @@
<script src="/bg/js/options.js"></script> <script src="/bg/js/options.js"></script>
<script src="/bg/js/page-exit-prevention.js"></script> <script src="/bg/js/page-exit-prevention.js"></script>
<script src="/bg/js/profile-conditions.js"></script> <script src="/bg/js/profile-conditions.js"></script>
<script src="/bg/js/templates.js"></script>
<script src="/bg/js/util.js"></script> <script src="/bg/js/util.js"></script>
<script src="/mixed/js/audio.js"></script> <script src="/mixed/js/audio.js"></script>

View File

@ -127,12 +127,12 @@ html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation
user-select: none; user-select: none;
} }
#query-parser { #query-parser-content {
margin-top: 0.5em; margin-top: 0.5em;
font-size: 2em; font-size: 2em;
} }
#query-parser[data-term-spacing=true] .query-parser-term { #query-parser-content[data-term-spacing=true] .query-parser-term {
margin-right: 0.2em; margin-right: 0.2em;
} }

View File

@ -105,6 +105,10 @@ function apiGetDisplayTemplatesHtml() {
return _apiInvoke('getDisplayTemplatesHtml'); return _apiInvoke('getDisplayTemplatesHtml');
} }
function apiGetQueryParserTemplatesHtml() {
return _apiInvoke('getQueryParserTemplatesHtml');
}
function apiGetZoom() { function apiGetZoom() {
return _apiInvoke('getZoom'); return _apiInvoke('getZoom');
} }

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
* Author: Alex Yatskov <alex@foosoft.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class TemplateHandler {
constructor(html) {
this._templates = new Map();
const doc = new DOMParser().parseFromString(html, 'text/html');
for (const template of doc.querySelectorAll('template')) {
this._setTemplate(template);
}
}
_setTemplate(template) {
const idMatch = template.id.match(/^([a-z-]+)-template$/);
if (!idMatch) {
throw new Error(`Invalid template ID: ${template.id}`);
}
this._templates.set(idMatch[1], template);
}
instantiate(name) {
const template = this._templates.get(name);
return document.importNode(template.content.firstChild, true);
}
instantiateFragment(name) {
const template = this._templates.get(name);
return document.importNode(template.content, true);
}
}

View File

@ -1,27 +0,0 @@
{{~#*inline "term"~}}
{{~#if preview~}}
<span class="query-parser-term-preview">
{{~else~}}
<span class="query-parser-term">
{{~/if~}}
{{~#each this~}}
{{> part }}
{{~/each~}}
</span>
{{~/inline~}}
{{~#*inline "part"~}}
{{~#if raw~}}
{{~#each text~}}
<span class="query-parser-char">{{this}}</span>
{{~/each~}}
{{~else~}}
<ruby>{{~#each text~}}
<span class="query-parser-char">{{this}}</span>
{{~/each~}}<rt>{{reading}}</rt></ruby>
{{~/if~}}
{{~/inline~}}
{{~#each terms~}}
{{> term preview=../preview }}
{{~/each~}}