Beter layouting
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
(displayln (git* next-version)))
|
(displayln (git* next-version)))
|
||||||
|
|
||||||
(target zip
|
(target zip
|
||||||
|
(deps clean)
|
||||||
(zip-package))
|
(zip-package))
|
||||||
|
|
||||||
(target clean
|
(target clean
|
||||||
|
|||||||
@@ -320,8 +320,22 @@ UI text is routed through `private/translate.rkt`. The server can be started wit
|
|||||||
racket main.rkt --data ./wiki-data --language nl
|
racket main.rkt --data ./wiki-data --language nl
|
||||||
```
|
```
|
||||||
|
|
||||||
Built-in English and Dutch translations are supplied. Administrators also get a **Translations** action. It opens the special page `wiki-translations-<language>`, for example `wiki-translations-nl`. Translation overrides use one `key = value` assignment per line. Saving that page is enough; the overrides are loaded from PostgreSQL on the next frontend load. Setup and login have built-in translations and therefore do not depend on that special page.
|
Built-in English and Dutch translations are supplied. Administrators also get a **Translations** action. It opens the single special page `wiki-translations`. When that page does not exist yet, the editor is prefilled with every built-in translation key. Each line can contain several languages, for example `save = nl:Opslaan, en:Save, de:Speichern`. Values containing commas or quotes can be quoted. The active language selects the matching value and falls back to `en` when the requested language is not present. Saving the page is enough; the overrides are loaded from PostgreSQL on the next frontend load. Setup and login retain their built-in translations and therefore do not depend on that special page.
|
||||||
|
|
||||||
Database schema 3 adds `todo_items`. Migration 2 -> 3 creates the table and indexes todo markers from all existing, non-archived pages. The migration is recorded in `wiki_schema` just like earlier schema changes.
|
Database schema 3 adds `todo_items`. Migration 2 -> 3 creates the table and indexes todo markers from all existing, non-archived pages. The migration is recorded in `wiki_schema` just like earlier schema changes.
|
||||||
|
|
||||||
The web setup also contains a UI-language selector. The selected language is stored in `wiki-data/language.rktd`; the command-line `--language` value is the default when that file does not yet exist.
|
The web setup also contains a UI-language selector. The selected language is stored in `wiki-data/language.rktd`; the command-line `--language` value is the default when that file does not yet exist.
|
||||||
|
|
||||||
|
|
||||||
|
## Version 0.2.12
|
||||||
|
|
||||||
|
Version 0.2.12 makes the first/start page title the visible wiki name and breadcrumb root. The account line is moved to the upper-right navigation area as compact links (`name · role · sign out · admin`), and page actions such as Edit, History and Delete use the same small link style as the breadcrumb. The breadcrumb/account/action chrome stays sticky while reading long pages. The sidebar keeps `* Todo list` directly below the wiki name; Users and Translations are reached through the Admin page.
|
||||||
|
|
||||||
|
Wiki image options now support placement in addition to width. Examples are `{width=50% center}`, `{width=320px right}`, `{width=320px right float}` and `{width=320px float=left}`. Without `float`, left/center/right controls block alignment. With float enabled, surrounding text can flow around left- or right-aligned images. Width remains limited to pixels and percentages and rendered images keep `max-width: 100%` and automatic height.
|
||||||
|
|
||||||
|
The special translations page is now one page named `wiki-translations`. It is prefilled from the built-in language tables and accepts several languages on one line, for example `save = nl:Opslaan, en:Save, de:Speichern`.
|
||||||
|
|
||||||
|
|
||||||
|
## Version 0.2.13
|
||||||
|
|
||||||
|
Version 0.2.13 centers the wiki name in the sidebar and makes it a link to the first/start page. The sticky top chrome now starts without top padding (`padding-top: 0`).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#lang info
|
#lang info
|
||||||
|
|
||||||
(define pkg-authors '(hnmdijkema))
|
(define pkg-authors '(hnmdijkema))
|
||||||
(define version "0.2.10")
|
(define version "0.2.14")
|
||||||
(define license 'MIT)
|
(define license 'MIT)
|
||||||
(define collection "racket-wiki")
|
(define collection "racket-wiki")
|
||||||
(define pkg-desc
|
(define pkg-desc
|
||||||
|
|||||||
@@ -121,8 +121,9 @@ for page-local checklists.
|
|||||||
@defproc[(tr [config any/c] [key symbol?]) string?] {
|
@defproc[(tr [config any/c] [key symbol?]) string?] {
|
||||||
Returns the effective UI translation for @racket[key]. Built-in English and
|
Returns the effective UI translation for @racket[key]. Built-in English and
|
||||||
Dutch translations are available before the database is configured. Once the
|
Dutch translations are available before the database is configured. Once the
|
||||||
database is available, assignments in the special
|
database is available, assignments in the special @tt{wiki-translations} page
|
||||||
@tt{wiki-translations-<language>} page override the built-in strings.
|
override the built-in strings. A line can contain several languages, for example
|
||||||
|
@tt{save = nl:Opslaan, en:Save, de:Speichern}.
|
||||||
}
|
}
|
||||||
|
|
||||||
The UI language is selected with @tt{--language} or @racket[#:language]. The
|
The UI language is selected with @tt{--language} or @racket[#:language]. The
|
||||||
|
|||||||
+2
-1
@@ -198,7 +198,8 @@ CSS
|
|||||||
(λ (_session)
|
(λ (_session)
|
||||||
(json-response
|
(json-response
|
||||||
(hash 'language (current-language config)
|
(hash 'language (current-language config)
|
||||||
'page (translation-page-slug (current-language config))
|
'page (translation-page-slug)
|
||||||
|
'template (translation-page-template)
|
||||||
'translations (translations-for config))))))
|
'translations (translations-for config))))))
|
||||||
|
|
||||||
(define (page-get-handler config req slug)
|
(define (page-get-handler config req slug)
|
||||||
|
|||||||
@@ -195,9 +195,16 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
|
display: block;
|
||||||
color: #111;
|
color: #111;
|
||||||
font-size: 1.18rem;
|
font-size: 1.18rem;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user-box,
|
#user-box,
|
||||||
@@ -795,3 +802,167 @@ body.editor-mode .editor-metadata-row {
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Top navigation -------------------------------------------------------- */
|
||||||
|
.top-chrome {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 120;
|
||||||
|
top: 0;
|
||||||
|
margin: -8px 0 20px;
|
||||||
|
padding: 0 0 10px;
|
||||||
|
background: white;
|
||||||
|
border-bottom: 1px solid #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: .5rem;
|
||||||
|
min-height: 1.35rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: .82rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-bar a,
|
||||||
|
.page-action-links a,
|
||||||
|
.admin-menu a,
|
||||||
|
.sidebar-primary-link {
|
||||||
|
color: var(--wiki-link);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-bar a:hover,
|
||||||
|
.page-action-links a:hover,
|
||||||
|
.admin-menu a:hover,
|
||||||
|
.sidebar-primary-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-row .breadcrumbs {
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-action-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: .65rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: .86rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-primary-link {
|
||||||
|
display: block;
|
||||||
|
margin: -4px 0 8px;
|
||||||
|
padding: 2px 0;
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: .65rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user-admin-view {
|
||||||
|
max-width: 1040px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wiki image layout extensions. Width itself remains a sanitized inline
|
||||||
|
style generated by wiki.js; these fixed classes only control placement. */
|
||||||
|
.markdown-body img.wiki-image-left,
|
||||||
|
.EasyMDEContainer .editor-preview img.wiki-image-left,
|
||||||
|
.EasyMDEContainer .editor-preview-side img.wiki-image-left {
|
||||||
|
display: block;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img.wiki-image-center,
|
||||||
|
.EasyMDEContainer .editor-preview img.wiki-image-center,
|
||||||
|
.EasyMDEContainer .editor-preview-side img.wiki-image-center {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img.wiki-image-right,
|
||||||
|
.EasyMDEContainer .editor-preview img.wiki-image-right,
|
||||||
|
.EasyMDEContainer .editor-preview-side img.wiki-image-right {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img.wiki-image-float-left,
|
||||||
|
.EasyMDEContainer .editor-preview img.wiki-image-float-left,
|
||||||
|
.EasyMDEContainer .editor-preview-side img.wiki-image-float-left {
|
||||||
|
float: left;
|
||||||
|
margin: .2rem 1rem .75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body img.wiki-image-float-right,
|
||||||
|
.EasyMDEContainer .editor-preview img.wiki-image-float-right,
|
||||||
|
.EasyMDEContainer .editor-preview-side img.wiki-image-float-right {
|
||||||
|
float: right;
|
||||||
|
margin: .2rem 0 .75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.top-chrome {
|
||||||
|
position: static;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-bar {
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: .35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-action-links {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 0.2.12 navigation polish --------------------------------------------- */
|
||||||
|
.account-separator {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Account and page actions are navigation, not push buttons. Keep them on
|
||||||
|
the same visual scale as the breadcrumb. */
|
||||||
|
.account-bar,
|
||||||
|
.page-action-links {
|
||||||
|
font-size: .86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-chrome {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the page navigation available while reading long documents too. */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.top-chrome {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 120;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+35
-17
@@ -15,13 +15,8 @@
|
|||||||
<div id="connection-status" class="connection-status hidden" role="status"></div>
|
<div id="connection-status" class="connection-status hidden" role="status"></div>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<aside id="sidebar">
|
<aside id="sidebar">
|
||||||
<div class="brand">Racket Wiki</div>
|
<a id="wiki-brand" class="brand" href="#">Racket Wiki</a>
|
||||||
<div id="user-box"></div>
|
<a id="todo-link" class="sidebar-primary-link" href="#">* <span data-tr="todo-list">Todo list</span></a>
|
||||||
<div class="sidebar-actions">
|
|
||||||
<button id="todo-button" data-tr="todo">Todo</button>
|
|
||||||
<button id="translations-button" class="admin-only hidden" data-tr="translations">Translations</button>
|
|
||||||
<button id="admin-button" class="admin-only hidden" data-tr="users">Users</button>
|
|
||||||
</div>
|
|
||||||
<form id="search-form" class="wiki-search" role="search">
|
<form id="search-form" class="wiki-search" role="search">
|
||||||
<input id="search-input" type="search" placeholder="Search wiki" data-tr-placeholder="search-wiki" autocomplete="off" aria-label="Search wiki">
|
<input id="search-input" type="search" placeholder="Search wiki" data-tr-placeholder="search-wiki" autocomplete="off" aria-label="Search wiki">
|
||||||
</form>
|
</form>
|
||||||
@@ -36,7 +31,25 @@
|
|||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main id="main">
|
<main id="main">
|
||||||
<nav id="breadcrumbs" class="breadcrumbs hidden" aria-label="Breadcrumb"></nav>
|
<div id="top-chrome" class="top-chrome">
|
||||||
|
<div id="account-bar" class="account-bar">
|
||||||
|
<span id="account-name"></span>
|
||||||
|
<span class="account-separator" aria-hidden="true">·</span>
|
||||||
|
<span id="account-role" class="muted"></span>
|
||||||
|
<span class="account-separator" aria-hidden="true">·</span>
|
||||||
|
<a id="logout-link" href="#" data-tr="sign-out">Sign out</a>
|
||||||
|
<span class="account-separator admin-only hidden" aria-hidden="true">·</span>
|
||||||
|
<a id="admin-link" class="admin-only hidden" href="#" data-tr="admin">Admin</a>
|
||||||
|
</div>
|
||||||
|
<div class="navigation-row">
|
||||||
|
<nav id="breadcrumbs" class="breadcrumbs hidden" aria-label="Breadcrumb"></nav>
|
||||||
|
<div id="page-action-links" class="page-action-links hidden">
|
||||||
|
<a id="edit-page" class="editor-only hidden" href="#" data-tr="edit">Edit</a>
|
||||||
|
<a id="history-page" href="#" data-tr="history">History</a>
|
||||||
|
<a id="delete-page" class="editor-only danger hidden" href="#" data-tr="delete">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section id="page-view" class="hidden">
|
<section id="page-view" class="hidden">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
@@ -44,11 +57,6 @@
|
|||||||
<h1 id="page-title"></h1>
|
<h1 id="page-title"></h1>
|
||||||
<div id="page-meta" class="muted"></div>
|
<div id="page-meta" class="muted"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar">
|
|
||||||
<button id="edit-page" class="editor-only hidden" data-tr="edit">Edit</button>
|
|
||||||
<button id="history-page" data-tr="history">History</button>
|
|
||||||
<button id="delete-page" class="editor-only danger hidden" data-tr="delete">Delete</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
<article id="markdown-preview" class="markdown-body"></article>
|
<article id="markdown-preview" class="markdown-body"></article>
|
||||||
<footer id="page-details" class="page-details"></footer>
|
<footer id="page-details" class="page-details"></footer>
|
||||||
@@ -109,18 +117,28 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="admin-view" class="hidden">
|
<section id="admin-view" class="hidden">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1 data-tr="admin">Admin</h1>
|
||||||
|
</header>
|
||||||
|
<nav class="admin-menu">
|
||||||
|
<a id="admin-users-link" href="#" data-tr="users">Users</a>
|
||||||
|
<a id="admin-translations-link" href="#" data-tr="translations">Translations</a>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="user-admin-view" class="hidden">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<h1 data-tr="user-administration">User administration</h1>
|
<h1 data-tr="user-administration">User administration</h1>
|
||||||
<button id="close-admin" data-tr="back">Back</button>
|
<a id="close-user-admin" href="#" data-tr="back">Back</a>
|
||||||
</header>
|
</header>
|
||||||
<form id="new-user-form" class="user-form">
|
<form id="new-user-form" class="user-form">
|
||||||
<input id="new-username" placeholder="Username" data-tr-placeholder="username" required>
|
<input id="new-username" placeholder="Username" data-tr-placeholder="username" required>
|
||||||
<input id="new-display-name" placeholder="Display name" data-tr-placeholder="display-name">
|
<input id="new-display-name" placeholder="Display name" data-tr-placeholder="display-name">
|
||||||
<input id="new-password" type="password" placeholder="Initial password" data-tr-placeholder="initial-password" required>
|
<input id="new-password" type="password" placeholder="Initial password" data-tr-placeholder="initial-password" required>
|
||||||
<select id="new-role">
|
<select id="new-role">
|
||||||
<option value="reader">reader</option>
|
<option value="reader" data-tr="role-reader">reader</option>
|
||||||
<option value="editor">editor</option>
|
<option value="editor" data-tr="role-editor">editor</option>
|
||||||
<option value="admin">admin</option>
|
<option value="admin" data-tr="role-admin">admin</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" data-tr="create-user">Create user</button>
|
<button type="submit" data-tr="create-user">Create user</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+129
-48
@@ -9,7 +9,9 @@
|
|||||||
newPageSlug: null,
|
newPageSlug: null,
|
||||||
previousView: "page-view",
|
previousView: "page-view",
|
||||||
translations: {},
|
translations: {},
|
||||||
translationPage: "wiki-translations-en"
|
translationPage: "wiki-translations",
|
||||||
|
translationTemplate: "",
|
||||||
|
siteTitle: "Racket Wiki"
|
||||||
};
|
};
|
||||||
|
|
||||||
let easyMDE = null;
|
let easyMDE = null;
|
||||||
@@ -17,9 +19,10 @@
|
|||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
function show(viewId) {
|
function show(viewId) {
|
||||||
for (const id of ["page-view", "not-found-view", "editor-view", "search-view", "todo-view", "history-view", "admin-view"]) {
|
for (const id of ["page-view", "not-found-view", "editor-view", "search-view", "todo-view", "history-view", "admin-view", "user-admin-view"]) {
|
||||||
$(id).classList.toggle("hidden", id !== viewId);
|
$(id).classList.toggle("hidden", id !== viewId);
|
||||||
}
|
}
|
||||||
|
$("page-action-links").classList.toggle("hidden", viewId !== "page-view");
|
||||||
document.body.classList.toggle("editor-mode", viewId === "editor-view");
|
document.body.classList.toggle("editor-mode", viewId === "editor-view");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +101,45 @@
|
|||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyImageWidthMarkup(html) {
|
||||||
|
const imageOptionsSuffix = /(<img\b[^>]*>)\s*\{\s*width\s*=\s*([0-9]+(?:\.[0-9]+)?)(%|px)?(?:\s+(left|center|right))?(?:\s+(float))?(?:\s+float\s*=\s*(left|right))?\s*\}/gi;
|
||||||
|
|
||||||
|
return (html || "").replace(imageOptionsSuffix, (_match, imageTag, amount, unit, align, floatFlag, floatSide) => {
|
||||||
|
const width = `${amount}${unit || "px"}`;
|
||||||
|
const closing = imageTag.endsWith("/>") ? "/>" : ">";
|
||||||
|
let tagBody = imageTag.slice(0, imageTag.length - closing.length);
|
||||||
|
const classes = [];
|
||||||
|
const effectiveFloatSide = floatSide || (floatFlag && (align === "left" || align === "right") ? align : null);
|
||||||
|
|
||||||
|
if (effectiveFloatSide) {
|
||||||
|
classes.push(`wiki-image-float-${effectiveFloatSide}`);
|
||||||
|
} else if (align) {
|
||||||
|
classes.push(`wiki-image-${align}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\sstyle=(['"])(.*?)\1/i.test(tagBody)) {
|
||||||
|
tagBody = tagBody.replace(/\sstyle=(['"])(.*?)\1/i, (_style, quote, value) =>
|
||||||
|
` style=${quote}${value}; width: ${width}${quote}`);
|
||||||
|
} else {
|
||||||
|
tagBody += ` style="width: ${width}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (classes.length > 0) {
|
||||||
|
if (/\sclass=(['"])(.*?)\1/i.test(tagBody)) {
|
||||||
|
tagBody = tagBody.replace(/\sclass=(['"])(.*?)\1/i, (_class, quote, value) =>
|
||||||
|
` class=${quote}${value} ${classes.join(" ")}${quote}`);
|
||||||
|
} else {
|
||||||
|
tagBody += ` class="${classes.join(" ")}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${tagBody}${closing}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderMarkdown(markdown) {
|
function renderMarkdown(markdown) {
|
||||||
return easyMDE.markdown(expandTodoMarkup(markdown || ""));
|
const html = easyMDE.markdown(expandTodoMarkup(markdown || ""));
|
||||||
|
return DOMPurify.sanitize(applyImageWidthMarkup(html));
|
||||||
}
|
}
|
||||||
|
|
||||||
function slugTitle(slug) {
|
function slugTitle(slug) {
|
||||||
@@ -261,7 +301,7 @@
|
|||||||
.then(onSuccess)
|
.then(onSuccess)
|
||||||
.catch((error) => onError(error.message));
|
.catch((error) => onError(error.message));
|
||||||
},
|
},
|
||||||
previewRender: (plainText) => easyMDE ? easyMDE.markdown(expandTodoMarkup(plainText)) : "",
|
previewRender: (plainText) => easyMDE ? renderMarkdown(plainText) : "",
|
||||||
renderingConfig: {
|
renderingConfig: {
|
||||||
codeSyntaxHighlighting: true,
|
codeSyntaxHighlighting: true,
|
||||||
hljs: window.hljs,
|
hljs: window.hljs,
|
||||||
@@ -474,7 +514,7 @@
|
|||||||
|
|
||||||
function pageBreadcrumbs(page, suffix = null) {
|
function pageBreadcrumbs(page, suffix = null) {
|
||||||
const items = [
|
const items = [
|
||||||
{ label: "Racket Wiki", href: "/" },
|
{ label: state.siteTitle, href: "/" },
|
||||||
{ label: page?.title || page?.slug || "New page" }
|
{ label: page?.title || page?.slug || "New page" }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -486,6 +526,25 @@
|
|||||||
renderBreadcrumbs(items);
|
renderBreadcrumbs(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startPage() {
|
||||||
|
const namedStartPage = state.pages.find((page) => page.slug === "start") || null;
|
||||||
|
if (namedStartPage) return namedStartPage;
|
||||||
|
if (state.pages.length === 0) return null;
|
||||||
|
|
||||||
|
return state.pages.reduce((first, page) => {
|
||||||
|
if (!first) return page;
|
||||||
|
return Number(page.createdAt) < Number(first.createdAt) ? page : first;
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWikiIdentity() {
|
||||||
|
const firstPage = startPage();
|
||||||
|
state.siteTitle = firstPage?.title || "Racket Wiki";
|
||||||
|
$("wiki-brand").textContent = state.siteTitle;
|
||||||
|
$("wiki-brand").href = firstPage ? `#/${encodeURIComponent(firstPage.slug)}` : "#";
|
||||||
|
document.title = state.siteTitle;
|
||||||
|
}
|
||||||
|
|
||||||
function renderPageList() {
|
function renderPageList() {
|
||||||
const list = $("page-list");
|
const list = $("page-list");
|
||||||
list.replaceChildren();
|
list.replaceChildren();
|
||||||
@@ -506,6 +565,7 @@
|
|||||||
async function loadPages() {
|
async function loadPages() {
|
||||||
const result = await api("/api/pages");
|
const result = await api("/api/pages");
|
||||||
state.pages = result.pages;
|
state.pages = result.pages;
|
||||||
|
updateWikiIdentity();
|
||||||
renderPageList();
|
renderPageList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,14 +605,7 @@
|
|||||||
$("editor-title").value = translationPage ? tr("translations", "Translations") : "";
|
$("editor-title").value = translationPage ? tr("translations", "Translations") : "";
|
||||||
$("editor-tags").value = "";
|
$("editor-tags").value = "";
|
||||||
if (translationPage) {
|
if (translationPage) {
|
||||||
const lines = [
|
easyMDE.value(state.translationTemplate || "");
|
||||||
"# Translation overrides",
|
|
||||||
"",
|
|
||||||
"Edit only the values after =. Unknown keys are ignored by the UI until they are used.",
|
|
||||||
""
|
|
||||||
];
|
|
||||||
Object.keys(state.translations).sort().forEach((key) => lines.push(`${key} = ${state.translations[key]}`));
|
|
||||||
easyMDE.value(lines.join("\n"));
|
|
||||||
} else {
|
} else {
|
||||||
easyMDE.value("");
|
easyMDE.value("");
|
||||||
}
|
}
|
||||||
@@ -620,6 +673,14 @@
|
|||||||
state.editingNew = false;
|
state.editingNew = false;
|
||||||
state.newPageSlug = null;
|
state.newPageSlug = null;
|
||||||
await loadPages();
|
await loadPages();
|
||||||
|
if (page.slug === state.translationPage) {
|
||||||
|
const translationData = await api("/api/translations");
|
||||||
|
state.language = translationData.language;
|
||||||
|
state.translationTemplate = translationData.template || state.translationTemplate;
|
||||||
|
state.translations = translationData.translations || {};
|
||||||
|
applyTranslations();
|
||||||
|
$("account-role").textContent = tr(`role-${state.session.user.role}`, state.session.user.role);
|
||||||
|
}
|
||||||
location.hash = `#/${encodeURIComponent(page.slug)}`;
|
location.hash = `#/${encodeURIComponent(page.slug)}`;
|
||||||
await openPage(page.slug);
|
await openPage(page.slug);
|
||||||
$("save-status").textContent = tr("saved", "Saved");
|
$("save-status").textContent = tr("saved", "Saved");
|
||||||
@@ -730,11 +791,12 @@
|
|||||||
state.currentPage = null;
|
state.currentPage = null;
|
||||||
await loadPages();
|
await loadPages();
|
||||||
if (state.pages.length > 0) {
|
if (state.pages.length > 0) {
|
||||||
location.hash = `#/${encodeURIComponent(state.pages[0].slug)}`;
|
const firstPage = startPage();
|
||||||
|
location.hash = `#/${encodeURIComponent(firstPage.slug)}`;
|
||||||
} else {
|
} else {
|
||||||
$("page-title").textContent = tr("no-pages", "No pages yet");
|
$("page-title").textContent = tr("no-pages", "No pages yet");
|
||||||
$("markdown-preview").innerHTML = "";
|
$("markdown-preview").innerHTML = "";
|
||||||
renderBreadcrumbs([{ label: "Racket Wiki" }]);
|
renderBreadcrumbs([{ label: state.siteTitle }]);
|
||||||
renderToc([], () => {});
|
renderToc([], () => {});
|
||||||
show("page-view");
|
show("page-view");
|
||||||
}
|
}
|
||||||
@@ -819,7 +881,7 @@
|
|||||||
|
|
||||||
const result = await api(`/api/search?q=${encodeURIComponent(text)}`);
|
const result = await api(`/api/search?q=${encodeURIComponent(text)}`);
|
||||||
renderBreadcrumbs([
|
renderBreadcrumbs([
|
||||||
{ label: "Racket Wiki", href: "/" },
|
{ label: state.siteTitle, href: "/" },
|
||||||
{ label: tr("search", "Search") }
|
{ label: tr("search", "Search") }
|
||||||
]);
|
]);
|
||||||
renderToc([], () => {});
|
renderToc([], () => {});
|
||||||
@@ -899,10 +961,22 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadAdmin() {
|
function showAdmin() {
|
||||||
state.previousView = state.currentPage ? "page-view" : "admin-view";
|
state.previousView = state.currentPage ? "page-view" : "admin-view";
|
||||||
renderBreadcrumbs([]);
|
renderBreadcrumbs([
|
||||||
|
{ label: state.siteTitle, href: "/" },
|
||||||
|
{ label: tr("admin", "Admin") }
|
||||||
|
]);
|
||||||
show("admin-view");
|
show("admin-view");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsersAdmin() {
|
||||||
|
renderBreadcrumbs([
|
||||||
|
{ label: state.siteTitle, href: "/" },
|
||||||
|
{ label: tr("admin", "Admin") },
|
||||||
|
{ label: tr("users", "Users") }
|
||||||
|
]);
|
||||||
|
show("user-admin-view");
|
||||||
const result = await api("/api/admin/users");
|
const result = await api("/api/admin/users");
|
||||||
const list = $("user-list");
|
const list = $("user-list");
|
||||||
list.replaceChildren();
|
list.replaceChildren();
|
||||||
@@ -917,7 +991,7 @@
|
|||||||
for (const roleName of ["reader", "editor", "admin"]) {
|
for (const roleName of ["reader", "editor", "admin"]) {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = roleName;
|
option.value = roleName;
|
||||||
option.textContent = roleName;
|
option.textContent = tr(`role-${roleName}`, roleName);
|
||||||
option.selected = roleName === user.role;
|
option.selected = roleName === user.role;
|
||||||
role.append(option);
|
role.append(option);
|
||||||
}
|
}
|
||||||
@@ -949,7 +1023,7 @@
|
|||||||
remove.addEventListener("click", async () => {
|
remove.addEventListener("click", async () => {
|
||||||
if (!confirm(`Delete user ${user.username}?`)) return;
|
if (!confirm(`Delete user ${user.username}?`)) return;
|
||||||
await api(`/api/admin/users/${user.id}`, { method: "DELETE" });
|
await api(`/api/admin/users/${user.id}`, { method: "DELETE" });
|
||||||
await loadAdmin();
|
await loadUsersAdmin();
|
||||||
});
|
});
|
||||||
actions.append(save, remove);
|
actions.append(save, remove);
|
||||||
row.append(username, display, role, enabled, password, actions);
|
row.append(username, display, role, enabled, password, actions);
|
||||||
@@ -963,7 +1037,7 @@
|
|||||||
state.newPageSlug = null;
|
state.newPageSlug = null;
|
||||||
$("not-found-message").textContent = `${tr("page-does-not-exist", "The page does not exist.")} (${slug})`;
|
$("not-found-message").textContent = `${tr("page-does-not-exist", "The page does not exist.")} (${slug})`;
|
||||||
renderBreadcrumbs([
|
renderBreadcrumbs([
|
||||||
{ label: "Racket Wiki", href: "/" },
|
{ label: state.siteTitle, href: "/" },
|
||||||
{ label: slug }
|
{ label: slug }
|
||||||
]);
|
]);
|
||||||
show("not-found-view");
|
show("not-found-view");
|
||||||
@@ -980,7 +1054,7 @@
|
|||||||
$("markdown-preview").innerHTML = "";
|
$("markdown-preview").innerHTML = "";
|
||||||
$("page-details").replaceChildren();
|
$("page-details").replaceChildren();
|
||||||
renderBreadcrumbs([
|
renderBreadcrumbs([
|
||||||
{ label: "Racket Wiki", href: "/" },
|
{ label: state.siteTitle, href: "/" },
|
||||||
{ label: slugTitle(slug) || slug }
|
{ label: slugTitle(slug) || slug }
|
||||||
]);
|
]);
|
||||||
show("page-view");
|
show("page-view");
|
||||||
@@ -1008,7 +1082,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.pages.length > 0) {
|
if (state.pages.length > 0) {
|
||||||
location.hash = `#/${encodeURIComponent(state.pages[0].slug)}`;
|
const firstPage = startPage();
|
||||||
|
location.hash = `#/${encodeURIComponent(firstPage.slug)}`;
|
||||||
} else {
|
} else {
|
||||||
if (can("editor")) {
|
if (can("editor")) {
|
||||||
showMissingEditablePage("start");
|
showMissingEditablePage("start");
|
||||||
@@ -1017,7 +1092,7 @@
|
|||||||
$("page-title").textContent = tr("no-pages", "No pages yet");
|
$("page-title").textContent = tr("no-pages", "No pages yet");
|
||||||
$("page-meta").textContent = "";
|
$("page-meta").textContent = "";
|
||||||
$("markdown-preview").innerHTML = "";
|
$("markdown-preview").innerHTML = "";
|
||||||
renderBreadcrumbs([{ label: "Racket Wiki" }]);
|
renderBreadcrumbs([{ label: state.siteTitle }]);
|
||||||
renderToc([], () => {});
|
renderToc([], () => {});
|
||||||
show("page-view");
|
show("page-view");
|
||||||
}
|
}
|
||||||
@@ -1027,7 +1102,7 @@
|
|||||||
async function showTodos() {
|
async function showTodos() {
|
||||||
state.previousView = state.currentPage ? "page-view" : "todo-view";
|
state.previousView = state.currentPage ? "page-view" : "todo-view";
|
||||||
renderBreadcrumbs([
|
renderBreadcrumbs([
|
||||||
{ label: "Racket Wiki", href: "/" },
|
{ label: state.siteTitle, href: "/" },
|
||||||
{ label: tr("todo", "Todo") }
|
{ label: tr("todo", "Todo") }
|
||||||
]);
|
]);
|
||||||
renderToc([], () => {});
|
renderToc([], () => {});
|
||||||
@@ -1097,24 +1172,12 @@
|
|||||||
const translationData = await api("/api/translations");
|
const translationData = await api("/api/translations");
|
||||||
state.language = translationData.language;
|
state.language = translationData.language;
|
||||||
state.translationPage = translationData.page;
|
state.translationPage = translationData.page;
|
||||||
|
state.translationTemplate = translationData.template || "";
|
||||||
state.translations = translationData.translations || {};
|
state.translations = translationData.translations || {};
|
||||||
applyTranslations();
|
applyTranslations();
|
||||||
initializeEditor();
|
initializeEditor();
|
||||||
const userBox = $("user-box");
|
$("account-name").textContent = state.session.user.displayName;
|
||||||
userBox.replaceChildren();
|
$("account-role").textContent = tr(`role-${state.session.user.role}`, state.session.user.role);
|
||||||
const displayName = document.createTextNode(state.session.user.displayName);
|
|
||||||
const breakNode = document.createElement("br");
|
|
||||||
const role = document.createElement("span");
|
|
||||||
role.className = "muted";
|
|
||||||
role.textContent = state.session.user.role;
|
|
||||||
const logout = document.createElement("button");
|
|
||||||
logout.id = "logout";
|
|
||||||
logout.textContent = tr("sign-out", "Sign out");
|
|
||||||
userBox.append(displayName, breakNode, role, " ", logout);
|
|
||||||
logout.addEventListener("click", async () => {
|
|
||||||
await api("/api/logout", { method: "POST", body: "{}" });
|
|
||||||
window.location.replace("/login");
|
|
||||||
});
|
|
||||||
updateRoleUi();
|
updateRoleUi();
|
||||||
await loadPages();
|
await loadPages();
|
||||||
await route();
|
await route();
|
||||||
@@ -1128,14 +1191,32 @@
|
|||||||
searchWiki($("search-input").value).catch((error) => console.error(error));
|
searchWiki($("search-input").value).catch((error) => console.error(error));
|
||||||
});
|
});
|
||||||
|
|
||||||
$("edit-page").addEventListener("click", beginEditPage);
|
$("edit-page").addEventListener("click", (event) => { event.preventDefault(); beginEditPage(); });
|
||||||
$("delete-page").addEventListener("click", deleteCurrentPage);
|
$("delete-page").addEventListener("click", (event) => { event.preventDefault(); deleteCurrentPage(); });
|
||||||
$("history-page").addEventListener("click", showHistory);
|
$("history-page").addEventListener("click", (event) => { event.preventDefault(); showHistory(); });
|
||||||
$("todo-button").addEventListener("click", () => showTodos().catch((error) => console.error(error)));
|
$("todo-link").addEventListener("click", (event) => { event.preventDefault(); showTodos().catch((error) => console.error(error)); });
|
||||||
$("translations-button").addEventListener("click", () => { location.hash = `#/${encodeURIComponent(state.translationPage)}`; });
|
$("logout-link").addEventListener("click", async (event) => {
|
||||||
$("admin-button").addEventListener("click", loadAdmin);
|
event.preventDefault();
|
||||||
|
await api("/api/logout", { method: "POST", body: "{}" });
|
||||||
|
window.location.replace("/login");
|
||||||
|
});
|
||||||
|
$("admin-link").addEventListener("click", (event) => { event.preventDefault(); showAdmin(); });
|
||||||
|
$("admin-users-link").addEventListener("click", (event) => { event.preventDefault(); loadUsersAdmin().catch(console.error); });
|
||||||
|
$("admin-translations-link").addEventListener("click", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const page = state.pages.find((item) => item.slug === state.translationPage);
|
||||||
|
if (page) {
|
||||||
|
location.hash = `#/${encodeURIComponent(state.translationPage)}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderBreadcrumbs([
|
||||||
|
{ label: state.siteTitle, href: "/" },
|
||||||
|
{ label: tr("translations", "Translations") }
|
||||||
|
]);
|
||||||
|
beginNewPage(state.translationPage);
|
||||||
|
});
|
||||||
$("close-history").addEventListener("click", () => show("page-view"));
|
$("close-history").addEventListener("click", () => show("page-view"));
|
||||||
$("close-admin").addEventListener("click", () => state.currentPage ? show("page-view") : route());
|
$("close-user-admin").addEventListener("click", (event) => { event.preventDefault(); showAdmin(); });
|
||||||
$("cancel-edit").addEventListener("click", () => {
|
$("cancel-edit").addEventListener("click", () => {
|
||||||
if (state.currentPage) {
|
if (state.currentPage) {
|
||||||
openPage(state.currentPage.slug);
|
openPage(state.currentPage.slug);
|
||||||
@@ -1167,7 +1248,7 @@
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
event.target.reset();
|
event.target.reset();
|
||||||
await loadAdmin();
|
await loadUsersAdmin();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("hashchange", () => route().catch(console.error));
|
window.addEventListener("hashchange", () => route().catch(console.error));
|
||||||
|
|||||||
+106
-13
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
(require db
|
(require db
|
||||||
racket/file
|
racket/file
|
||||||
|
racket/list
|
||||||
racket/string
|
racket/string
|
||||||
"private/config.rkt"
|
"private/config.rkt"
|
||||||
"private/database.rkt")
|
"private/database.rkt")
|
||||||
@@ -10,11 +11,12 @@
|
|||||||
translations-for
|
translations-for
|
||||||
current-language
|
current-language
|
||||||
write-language!
|
write-language!
|
||||||
translation-page-slug)
|
translation-page-slug
|
||||||
|
translation-page-template)
|
||||||
|
|
||||||
(define english
|
(define english
|
||||||
(hash
|
(hash
|
||||||
'users "Users" 'translations "Translations" 'todo "Todo"
|
'users "Users" 'translations "Translations" 'todo "Todo" 'todo-list "Todo list" 'admin "Admin" 'role-reader "reader" 'role-editor "editor" 'role-admin "admin"
|
||||||
'search-wiki "Search wiki" 'contents "Contents" 'pages "Pages"
|
'search-wiki "Search wiki" 'contents "Contents" 'pages "Pages"
|
||||||
'edit "Edit" 'history "History" 'delete "Delete" 'page-not-found "Page not found"
|
'edit "Edit" 'history "History" 'delete "Delete" 'page-not-found "Page not found"
|
||||||
'cancel "Cancel" 'save "Save" 'page-title "Page title" 'tags "Tags (comma separated)"
|
'cancel "Cancel" 'save "Save" 'page-title "Page title" 'tags "Tags (comma separated)"
|
||||||
@@ -47,14 +49,14 @@
|
|||||||
|
|
||||||
(define dutch
|
(define dutch
|
||||||
(hash
|
(hash
|
||||||
'users "Gebruikers" 'translations "Vertalingen" 'todo "Todo"
|
'users "Gebruikers" 'translations "Vertalingen" 'todo "Todo" 'todo-list "Todo-lijst" 'admin "Admin" 'role-reader "lezer" 'role-editor "redacteur" 'role-admin "admin"
|
||||||
'search-wiki "Wiki doorzoeken" 'contents "Inhoud" 'pages "Pagina's"
|
'search-wiki "Wiki doorzoeken" 'contents "Inhoud" 'pages "Pagina's"
|
||||||
'edit "Bewerken" 'history "Geschiedenis" 'delete "Verwijderen" 'page-not-found "Pagina niet gevonden"
|
'edit "Bewerken" 'history "Geschiedenis" 'delete "Verwijderen" 'page-not-found "Pagina niet gevonden"
|
||||||
'cancel "Annuleren" 'save "Opslaan" 'page-title "Paginatitel" 'tags "Tags (komma-gescheiden)"
|
'cancel "Annuleren" 'save "Opslaan" 'page-title "Paginatitel" 'tags "Tags (komma-gescheiden)"
|
||||||
'version-summary "Versiesamenvatting (optioneel)" 'search "Zoeken" 'page-history "Paginageschiedenis"
|
'version-summary "Versiesamenvatting (optioneel)" 'search "Zoeken" 'page-history "Paginageschiedenis"
|
||||||
'back "Terug" 'user-administration "Gebruikersbeheer" 'username "Gebruikersnaam"
|
'back "Terug" 'user-administration "Gebruikersbeheer" 'username "Gebruikersnaam"
|
||||||
'display-name "Weergavenaam" 'initial-password "Initieel wachtwoord" 'create-user "Gebruiker aanmaken"
|
'display-name "Weergavenaam" 'initial-password "Initieel wachtwoord" 'create-user "Gebruiker aanmaken"
|
||||||
'sign-in "Inloggen" 'sign-out "Uitloggen" 'password "Wachtwoord"
|
'sign-in "Inloggen" 'sign-out "Afmelden" 'password "Wachtwoord"
|
||||||
'upload-image "Afbeelding uploaden" 'upload-file "Bestand uploaden" 'saved "Opgeslagen"
|
'upload-image "Afbeelding uploaden" 'upload-file "Bestand uploaden" 'saved "Opgeslagen"
|
||||||
'offline "Offline" 'online "Online" 'todo-items "Todo-items"
|
'offline "Offline" 'online "Online" 'todo-items "Todo-items"
|
||||||
'no-todos "Geen todo-items." 'no-matching-pages "Geen overeenkomende pagina's."
|
'no-todos "Geen todo-items." 'no-matching-pages "Geen overeenkomende pagina's."
|
||||||
@@ -80,6 +82,7 @@
|
|||||||
|
|
||||||
(define (base-translations language)
|
(define (base-translations language)
|
||||||
(if (string-ci=? language "nl") dutch english))
|
(if (string-ci=? language "nl") dutch english))
|
||||||
|
|
||||||
(define (current-language config)
|
(define (current-language config)
|
||||||
(with-handlers ((exn:fail? (λ (_e) (wiki-config-language config))))
|
(with-handlers ((exn:fail? (λ (_e) (wiki-config-language config))))
|
||||||
(if (file-exists? (language-config-path config))
|
(if (file-exists? (language-config-path config))
|
||||||
@@ -98,9 +101,85 @@
|
|||||||
#:exists 'truncate/replace)
|
#:exists 'truncate/replace)
|
||||||
(void))
|
(void))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
; goal : Return the slug of the special translations page.
|
||||||
|
; pre : none; language is accepted for compatibility with older callers.
|
||||||
|
; post : no state is changed.
|
||||||
|
; result : The fixed wiki-translations slug.
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
(define (translation-page-slug [language #f])
|
||||||
|
"wiki-translations")
|
||||||
|
|
||||||
(define (translation-page-slug language)
|
(define (translation-value-needs-quotes? value)
|
||||||
(string-append "wiki-translations-" (string-downcase language)))
|
(or (string-contains? value ",")
|
||||||
|
(string-contains? value "\"")
|
||||||
|
(not (string=? value (string-trim value)))))
|
||||||
|
|
||||||
|
(define (translation-value->text value)
|
||||||
|
(if (translation-value-needs-quotes? value)
|
||||||
|
(string-append "\""
|
||||||
|
(string-replace (string-replace value "\\" "\\\\") "\"" "\\\"")
|
||||||
|
"\"")
|
||||||
|
value))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
; goal : Build the initial contents of the special translations page.
|
||||||
|
; pre : the built-in translation tables are available.
|
||||||
|
; post : no state is changed.
|
||||||
|
; result : Text containing every known key with nl and en values.
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
(define (translation-page-template)
|
||||||
|
(define keys
|
||||||
|
(sort (remove-duplicates (append (hash-keys english) (hash-keys dutch)))
|
||||||
|
string<?
|
||||||
|
#:key symbol->string))
|
||||||
|
(string-join
|
||||||
|
(for/list ((key (in-list keys)))
|
||||||
|
(format "~a = nl:~a, en:~a"
|
||||||
|
(symbol->string key)
|
||||||
|
(translation-value->text (hash-ref dutch key (hash-ref english key "")))
|
||||||
|
(translation-value->text (hash-ref english key ""))))
|
||||||
|
"\n"))
|
||||||
|
|
||||||
|
(define (unquote-translation-value value)
|
||||||
|
(define text (string-trim value))
|
||||||
|
(if (and (>= (string-length text) 2)
|
||||||
|
(char=? (string-ref text 0) #\")
|
||||||
|
(char=? (string-ref text (sub1 (string-length text))) #\"))
|
||||||
|
(let ((body (substring text 1 (sub1 (string-length text)))))
|
||||||
|
(string-replace (string-replace body "\\\"" "\"") "\\\\" "\\"))
|
||||||
|
text))
|
||||||
|
|
||||||
|
(define (split-translation-variants text)
|
||||||
|
(let loop ((i 0)
|
||||||
|
(start 0)
|
||||||
|
(quoted? #f)
|
||||||
|
(escaped? #f)
|
||||||
|
(result '()))
|
||||||
|
(if (>= i (string-length text))
|
||||||
|
(reverse (cons (substring text start i) result))
|
||||||
|
(let ((ch (string-ref text i)))
|
||||||
|
(cond
|
||||||
|
(escaped?
|
||||||
|
(loop (add1 i) start quoted? #f result))
|
||||||
|
((and quoted? (char=? ch #\\))
|
||||||
|
(loop (add1 i) start quoted? #t result))
|
||||||
|
((char=? ch #\")
|
||||||
|
(loop (add1 i) start (not quoted?) #f result))
|
||||||
|
((and (not quoted?) (char=? ch #\,))
|
||||||
|
(loop (add1 i) (add1 i) #f #f (cons (substring text start i) result)))
|
||||||
|
(else
|
||||||
|
(loop (add1 i) start quoted? #f result)))))))
|
||||||
|
|
||||||
|
(define (parse-translation-variants text)
|
||||||
|
(for/fold ((result (hash)))
|
||||||
|
((part (in-list (split-translation-variants text))))
|
||||||
|
(define match (regexp-match #px"^\\s*([A-Za-z][A-Za-z0-9_-]*)\\s*:(.*)$" part))
|
||||||
|
(if match
|
||||||
|
(hash-set result
|
||||||
|
(string-downcase (list-ref match 1))
|
||||||
|
(unquote-translation-value (list-ref match 2)))
|
||||||
|
result)))
|
||||||
|
|
||||||
(define (parse-overrides markdown)
|
(define (parse-overrides markdown)
|
||||||
(for/fold ((result (hash)))
|
(for/fold ((result (hash)))
|
||||||
@@ -108,9 +187,10 @@
|
|||||||
(define match
|
(define match
|
||||||
(regexp-match #px"^\\s*([A-Za-z0-9._-]+)\\s*=\\s*(.*?)\\s*$" line))
|
(regexp-match #px"^\\s*([A-Za-z0-9._-]+)\\s*=\\s*(.*?)\\s*$" line))
|
||||||
(if match
|
(if match
|
||||||
(hash-set result
|
(let ((variants (parse-translation-variants (list-ref match 2))))
|
||||||
(string->symbol (list-ref match 1))
|
(if (zero? (hash-count variants))
|
||||||
(list-ref match 2))
|
result
|
||||||
|
(hash-set result (string->symbol (list-ref match 1)) variants)))
|
||||||
result)))
|
result)))
|
||||||
|
|
||||||
(define (database-overrides config)
|
(define (database-overrides config)
|
||||||
@@ -123,9 +203,18 @@
|
|||||||
(define markdown
|
(define markdown
|
||||||
(query-maybe-value db
|
(query-maybe-value db
|
||||||
"SELECT markdown FROM pages WHERE slug = $1 AND archived = FALSE"
|
"SELECT markdown FROM pages WHERE slug = $1 AND archived = FALSE"
|
||||||
(translation-page-slug (current-language config))))
|
(translation-page-slug)))
|
||||||
(if markdown (parse-overrides markdown) (hash)))))))
|
(if markdown (parse-overrides markdown) (hash)))))))
|
||||||
|
|
||||||
|
(define (translation-override-for-language variants language)
|
||||||
|
(define language-key (string-downcase language))
|
||||||
|
(cond
|
||||||
|
((hash-has-key? variants language-key)
|
||||||
|
(hash-ref variants language-key))
|
||||||
|
((hash-has-key? variants "en")
|
||||||
|
(hash-ref variants "en"))
|
||||||
|
(else #f)))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
; goal : Return all translations active for the configured wiki language.
|
; goal : Return all translations active for the configured wiki language.
|
||||||
; pre : config is a wiki-config value.
|
; pre : config is a wiki-config value.
|
||||||
@@ -133,9 +222,13 @@
|
|||||||
; result : A hash from translation symbols to strings.
|
; result : A hash from translation symbols to strings.
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
(define (translations-for config)
|
(define (translations-for config)
|
||||||
(for/fold ((result (base-translations (current-language config))))
|
(define language (current-language config))
|
||||||
(((key value) (in-hash (database-overrides config))))
|
(for/fold ((result (base-translations language)))
|
||||||
(hash-set result key value)))
|
(((key variants) (in-hash (database-overrides config))))
|
||||||
|
(define value (translation-override-for-language variants language))
|
||||||
|
(if value
|
||||||
|
(hash-set result key value)
|
||||||
|
result)))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
; goal : Translate one UI key for the configured wiki language.
|
; goal : Translate one UI key for the configured wiki language.
|
||||||
|
|||||||
Reference in New Issue
Block a user