Skip to content
Snippets Groups Projects
Commit 2aaa94f5 authored by Philipp Stadler's avatar Philipp Stadler
Browse files

feat!: lazy load notice text and add field for custom credits

BREAKING CHANGE: Adds a new `Credits` field, one-way sync needed after import
parent a5d21a05
No related branches found
No related tags found
1 merge request!5feat!: lazy load notice text and add field for custom credits
Pipeline #14028 passed
Showing
with 98 additions and 164 deletions
......@@ -5,7 +5,6 @@ module.exports = function (app) {
if (!('BUILD_PREFIX' in process.env)) {
throw new Error(`Dunno where the chardata at, what BUILD_PREFIX?`)
}
const dir = `${process.env.BUILD_PREFIX}hanzi-data`
app.use("/hanzi-data", serveStatic(dir))
console.error(('ok configured at '), dir)
app.use(`/${process.env.BUILD_PREFIX}`, serveStatic(process.env.BUILD_PREFIX))
console.error(`data at /${process.env.BUILD_PREFIX}`)
}
File moved
RELEASE_TAR := $(BUILD_PREFIX)card-templates-$(shell grep '"version":' package.json -m 1 | cut -d '"' -f 4).tar.gz
RELEASE_INCLUDE_SRC := LICENSE NOTICE
RELEASE_INCLUDE_SRC :=
RELEASE_INCLUDE_ARTIFACTS := $(HANZI_DATA) $(BUILT_TEMPLATE_HTMLS) $(BUILT_TEMPLATE_SPECS) $(JSONP_OUTPUT)
RELEASE_CONTENT_SRC := $(addprefix $(RELEASE_DIR)/,$(RELEASE_INCLUDE_SRC))
RELEASE_CONTENT_ARTIFACTS := $(patsubst $(BUILD_PREFIX)%,$(RELEASE_DIR)/%,$(RELEASE_INCLUDE_ARTIFACTS))
......
......@@ -22,7 +22,7 @@ DEEP_CLEAN_DIRS += .parcel-cache dist
html: $(BUILT_TEMPLATE_HTMLS) $(BUILT_TEMPLATE_SPECS)
.PHONY: dev
dev: $(PARCEL) $(HANZI_DATA)
dev: $(PARCEL) $(HANZI_DATA) $(JSONP_OUTPUT)
$(PARCEL) serve $(PARCEL_DEV_ENTRY_HTMLS) --config=./$(PARCEL_DEV_CONFIG)
.PHONY: test-html
......@@ -35,7 +35,7 @@ $(HTML_TEST_OK_FLAG): $(JEST) $(HANZI_DATA) $(SRC_TEMPLATE_TESTS) $(SRC_TEMPLATE
# no parcel plugin or config required for production builds, Anki handles the
# tags
$(BUILT_TEMPLATE_HTMLS) &: $(HANZI_DATA) $(PARCEL) $(TEMPLATE_FILES) $(SRC_COMMON_TS)
$(BUILT_TEMPLATE_HTMLS) &: $(HANZI_DATA) $(JSONP_OUTPUT) $(PARCEL) $(TEMPLATE_FILES) $(SRC_COMMON_TS)
@$(and $(BUILD_PREFIX),mkdir -p $(BUILD_PREFIX))
$(PARCEL) build --no-source-maps --dist-dir=$(BUILD_PREFIX)templates $(SRC_TEMPLATE_HTMLS)
# set moddat even for files that did not need to be updated (to avoid redundant rebuilds)
......
JSONP_INPUT := NOTICE
JSONP_OUTPUT := $(addprefix $(BUILD_PREFIX)_hd,$(addsuffix .js,$(JSONP_INPUT)))
JSONP_INPUT := KA_TEMPLATES_NOTICE
JSONP_OUTPUT := $(foreach I,$(JSONP_INPUT),$(BUILD_PREFIX)$(I).js)
MOSTLY_CLEAN += $(JSONP_OUTPUT)
.PHONY: jsonp
jsonp: $(JSONP_OUTPUT)
$(JSONP_OUTPUT): $(BUILD_PREFIX)_hd%.js: % $(JSONP)
$(JSONP_OUTPUT): $(BUILD_PREFIX)%.js: % $(JSONP)
$(JSONP) --fn hd $< -f $@
......@@ -3,7 +3,7 @@ ESLINT := node_modules/.bin/eslint
JEST := node_modules/.bin/jest
TSC := node_modules/.bin/tsc
JSONP := node_modules/.bin/to-static-jsonp
NODE_BINS := $(PARCEL) $(ESLINT) $(JEST) $(TSC) $(JSONP) $(MARKDOWN_TO_HTML)
NODE_BINS := $(PARCEL) $(ESLINT) $(JEST) $(TSC) $(JSONP)
GEN_HANZI_DATA := $(BUILD_PREFIX)build/gen-hanzi-data/run
BUILD_TS_SOURCES := $(shell find build -name '*.ts')
......
{
"name": "kartenaale-card-templates",
"version": "1.0.2",
"version": "2.0.0",
"description": "HTML and data for Anki cards",
"repository": "https://gitlab.phaidra.org/kartenaale/card-templates",
"author": "Philipp Stadler <hello@phstadler.com>",
......
import { readFile } from 'fs/promises'
export interface GetDataOpts {
/**
* The JS file to load for the data, e.g. `hanzi-data/_hdA.js'.
*
* Directories are handled in testing, but only the basename is considered in
* Anki, which does not support directories.
*/
path: string
/**
* First argument to the JSONP callback before the actual data.
*/
key: string
}
const dataFilePrefix = '_hd'
const jsonpCallbackFnName = 'hd'
/** keys against functions to call on resolve. */
const jsonpWaiters = new Map<string, Array<(unknown) => void>>()
......@@ -21,22 +30,19 @@ const canAccessFilesystem = typeof document === 'undefined'
* Unsafe because it is up to the caller to cast to a concrete correct type.
*/
export async function getDataUnsafe (
{ key }: GetDataOpts
opts: GetDataOpts
): Promise<Readonly<unknown>> {
if (key.length !== 1) {
throw new Error(`Can only get data for single chars, got: ${key}`)
}
const { key } = opts
let data: Readonly<unknown>
if (key in cached) {
data = cached[key]
} else if (canAccessFilesystem) {
// read directly from disk when running in test and skip the cache
data = await fetchNonBrowser(key)
data = await fetchNonBrowser(opts)
} else {
// in production and parcel use a JSONP-like API because everything else
// failed in AnkiDroid
data = await fetchJsonp(key)
data = await fetchJsonp(opts)
// caching helps a lot with load speed since the same character often
// occurs multiple times on the same card
cached[key] = data
......@@ -47,15 +53,14 @@ export async function getDataUnsafe (
/**
* Fetches the JSONP via direct filesystem access (for tests).
*/
async function fetchNonBrowser (key: string): Promise<Readonly<unknown>> {
async function fetchNonBrowser (opts: GetDataOpts): Promise<Readonly<unknown>> {
if (!('BUILD_PREFIX' in process.env)) {
throw new Error('Dunno where the data at, what BUILD_PREFIX?')
}
const dir = `${process.env.BUILD_PREFIX}hanzi-data`
const path = `${dir}/${dataFilePrefix}${key}.js`
const path = `${process.env.BUILD_PREFIX}${opts.path}`
const jsonp = await readFile(path, { encoding: 'utf-8' })
const data = JSON.parse(jsonp.substring(
`hd("${key}",`.length,
`${jsonpCallbackFnName}(${JSON.stringify(opts.key)},`.length,
jsonp.length - ')'.length
))
// just for testing: we want to catch accidental modifications of data that
......@@ -103,23 +108,22 @@ function registerJsonpWaiter (
/**
* Fetches data over the network for the specified key.
*/
async function fetchJsonp (char: string): Promise<Readonly<unknown>> {
async function fetchJsonp (opts: GetDataOpts): Promise<Readonly<unknown>> {
return await new Promise((resolve, reject) => {
const prefix = process.env.NODE_ENV === 'production'
const url = process.env.NODE_ENV === 'production'
// No directories for Anki media possible, for prod use just filename
? ''
? opts.path.split('/').pop()!
// For testing in parcel it works better to have a subpath that we can
// exclude from parcel.
: '/hanzi-data/'
const url = `${prefix}${dataFilePrefix}${char}.js`
: `/${process.env.BUILD_PREFIX}${opts.path}`
if (document.querySelector(`script[src="${url}"]`) !== null) {
// already being fetched, append to the list of existing waiters
registerJsonpWaiter(char, resolve)
registerJsonpWaiter(opts.key, resolve)
} else {
// no fetch in flight, inject a new script tag
const script = document.createElement('script')
registerJsonpWaiter(
char,
opts.key,
data => {
script.remove()
resolve(data)
......@@ -129,7 +133,7 @@ async function fetchJsonp (char: string): Promise<Readonly<unknown>> {
script.src = url
script.onerror = () => {
script.remove()
reject(new Error(`No character data available for ${char}`))
reject(new Error(`No character data available for ${opts.key}`))
}
document.body.appendChild(script)
}
......
@import url(./theme.css);
@import url(./debug/debug.css);
@import url(./fira.css);
@import url(./notice/notice.css);
@import url(./tts/tts.css);
@import url(./write/write.css);
@import url(./answer-details.css);
......
......@@ -30,7 +30,7 @@ export async function getHanziData (
throw new Error(`Can only get data for single chars, got: ${char}`)
}
const data = (await getDataUnsafe({ key: char })) as Readonly<HanziData>
const data = (await getDataUnsafe({ path: `hanzi-data/_hd${char}.js`, key: char })) as Readonly<HanziData>
if (kind === GetHanziDataKind.TRADITIONAL && data.trad !== undefined) {
// data in the cache is read-only => make a fresh copy with trad overrides
return {
......
import { init as initHanziData } from './hanzi-data/init'
import { initNotice } from './notice'
import { init as initTts } from './tts/init'
import { init as initWrite } from './write/init'
......@@ -9,4 +10,5 @@ export async function init (within: HTMLElement | null): Promise<void> {
await initHanziData(within)
initTts(within)
await initWrite(within)
await initNotice(within)
}
import { getDataUnsafe } from "./data";
export async function loadText(filename: string): Promise<string> {
return (await getDataUnsafe({ key: filename })) as string
const data = await getDataUnsafe({ path: filename + '.js', key: filename })
if (typeof data !== 'string') {
throw new Error(`${filename} loaded, but is not text`)
}
return toHtml(data)
}
function toHtml(raw: string): string {
return raw.split(/[\n-]{2,}/).map(p => `<p>${
p.replace(/([a-z]+:\/\/[/a-zA-Z-.]+[/a-zA-Z])/g, '<a href="$1">$1</a>')
}</p>`).join('')
}
import { loadText } from "./load-text"
export async function initNotice(within: HTMLElement) {
within.querySelectorAll('[notice-file]').forEach(
async el => {
const noticeFile = el.getAttribute('notice-file') ?? ''
if (noticeFile !== '') {
el.innerHTML = await loadText(noticeFile)
}
}
)
}
.notice-contents {
text-align: justify;
}
.notice-contents ul {
list-style-type: none;
}
<details class="answer-details">
<summary>
<span class="answer-details-more">About this deck…</span>
<span class="answer-details-less">Hide notice…</span>
</summary>
<aside class="notice-contents">
<p>
The code that makes this deck work is open source software available on
<a href="https://gitlab.phaidra.org/kartenaale/sinologie-anki-pack">University of Vienna GitLab</a>.
<br>Check out releases for more decks and updates, or file an issue if
you are experiencing issues or if you want to contribute.
</p>
<p>
Deck: Chinese Cultural Spaces<br>
Special thanks go out to Chris for making this deck!<br>
Please respect the rights of the lecturer that provided the content, as well as
of the photographers that made the images. Share the deck only with your fellow
students.
</p>
<p>
Decks: Chinesisch lernen für die Sinologie, Band 1&2<br>
Stefanie Yu kindly gave permission to make an Anki version of her vocabulary on
Quizlet that can be freely shared among students of the University of Vienna.
It is available with releases of this project on GitLab, and you are welcome to
improve on it and share your improvements with other students.<br>
The vocabulary and some additional content like stroke names are taken from the
book "Chinesisch lernen für die Sinologie, Band 1" by Xia Baige and Wolfgang
Zeidl. Xia Baige kindly gave permission to use their work in this Anki deck.
The book itself is available at Universität Wien.<br>
Please respect the rights of the original authors and share content from
Sinologie Anki Pack only with your fellow students.
</p>
<p>
Deck: Introduction to Chinese Cultural History<br>
This deck includes content from the slides of the lecture _Introduction to
Chinese Cultural History_ at Universität Wien, held by Dr. Rossella Ferrari.<br>
A separate part of the deck heavily relies on content from _The Columbia
Companion to Modern Chinese Literature_.<br>
Please respect the rights of the original authors and share this deck only with
your fellow students.
</p>
<p>
Deck: Einführung in die politische Geschichte Chinas<br>
This deck includes content from the slides of the lecture _Einführung in die
politische Geschichte Chinas_ at Universität Wien, held by Dr. Christian Göbel.<br>
Please respect the rights of the original authors and share this deck only with
your fellow students.
</p>
<p>
Deck: Wirtschaftliche Entwicklung Chinas<br>
This deck includes content from the slides of the lecture
_Wirtschaftliche Entwicklung Chinas_ at Universität Wien, held by Dr.
Christian Göbel.<br>
Please respect the rights of the original authors and share this deck only with
your fellow students.
</p>
<p>
German and Chinese radical names are taken from the German version of
Wikipedia. A hearty thank you to all authors and maintainers.
</p>
<p>
This product includes software from the hanzi-writer project (MIT).
<br>Copyright (c) 2014 David Chanin.
<br><a href="https://github.com/chanind/hanzi-writer">https://github.com/chanind/hanzi-writer</a>
</p>
<p>
COPYING notice from the hanzi-writer project:
Hanzi Writer uses data from the excellent Make Me a Hanzi project and fonts from Arphic. You can redistribute
and/or
modify the font data under the terms of the Arphic Public License as published by Arphic Technology Co., Ltd. You
should
have received a copy of this license (the directory "APL") in the data folder of this repository; if not, see
http://ftp.gnu.org/non-gnu/chinese-fonts-truetype/LICENSE.
Arphic PL KaitiM GB and UKai Copyright 1999 Arphic Technology Co., Ltd.; licensed under the Arphic Public License
http://www.arphic.com.tw/en/home/index
Make Me a Hanzi Copyright 1999 Arphic Technology Co., Ltd., copyright 2016 Shaunak Kishore; licensed under the
Arphic
Public License https://github.com/skishore/makemeahanzi
</p>
<p>
This product includes software from the cnchar project (MIT).
<br>Copyright (c) 2017 - present theajack &lt;theajack@qq.com&gt;
<br><a href="https://github.com/theajack/cnchar">https://github.com/theajack/cnchar</a>
</p>
<p>
This product includes software from the hanzi-tool project (MIT).
<br>Copyright (c) 2012-2014 Jeremiah Daneil de la Rouviere
<br><a href="https://github.com/ginsudev/hanzi">https://github.com/ginsudev/hanzi</a>
</p>
<p>
This project also would not have been possible without these projects and
the teams behind them:
</p>
<ul>
<li>Anki (<a href="https://apps.ankiweb.net/">https://apps.ankiweb.net/</a>)</li>
<li>parcel (<a href="https://parceljs.org/">https://parceljs.org/</a>)</li>
</ul>
</aside>
</details>
\ No newline at end of file
template_version: 2024-02-28 22:00:00+00:00
template_version: 2024-07-20 10:00:00+00:00
note_type:
id: 2024-01-05 03:00:00+00:00
......@@ -6,6 +6,7 @@ note_type:
fields:
- A
- B
- Credits
card_types:
- name: Forward
......@@ -13,4 +14,5 @@ card_types:
- name: Backward
template: backward
resource_paths: []
resource_paths:
- '{{BUILD_PREFIX}}KA_TEMPLATES_NOTICE.js'
......@@ -9,7 +9,15 @@
{{A}}
</div>
<include src="src/components/notice/notice.html"></include>
<details class="answer-details">
<summary>
<span class="answer-details-more">Über das Pack…</span>
<span class="answer-details-less">Info verbergen…</span>
</summary>
{{Credits}}
<div notice-file="KA_TEMPLATES_NOTICE"></div>
</details>
</div>
<!-- Suppress speech output on AnkiDroid if globally enabled -->
......
......@@ -9,7 +9,15 @@
{{B}}
</div>
<include src="src/components/notice/notice.html"></include>
<details class="answer-details">
<summary>
<span class="answer-details-more">Über das Pack…</span>
<span class="answer-details-less">Info verbergen…</span>
</summary>
{{Credits}}
<div notice-file="KA_TEMPLATES_NOTICE"></div>
</details>
</div>
<!-- Suppress speech output on AnkiDroid if globally enabled -->
......
template_version: 2024-02-28 22:00:00+00:00
template_version: 2024-07-20 10:00:00+00:00
note_type:
id: 2024-01-04 03:00:00+00:00
......@@ -6,9 +6,11 @@ note_type:
fields:
- Front
- Back
- Credits
card_types:
- name: Q/A
template: q_a
resource_paths: []
resource_paths:
- '{{BUILD_PREFIX}}KA_TEMPLATES_NOTICE.js'
......@@ -9,7 +9,15 @@
{{Back}}
</div>
<include src="src/components/notice/notice.html"></include>
<details class="answer-details">
<summary>
<span class="answer-details-more">Über das Pack…</span>
<span class="answer-details-less">Info verbergen…</span>
</summary>
{{Credits}}
<div notice-file="KA_TEMPLATES_NOTICE"></div>
</details>
</div>
<!-- Suppress speech output on AnkiDroid if globally enabled -->
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment