Merge remote-tracking branch 'origin/main' into standalone

This commit is contained in:
Elian Doran
2026-04-09 12:57:06 +03:00
40 changed files with 514 additions and 345 deletions

View File

@@ -47,6 +47,14 @@ document.addEventListener(
if (toggleMenuButton && layout) {
toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu"));
}
// Format <time> elements using the browser's locale.
for (const el of document.querySelectorAll<HTMLTimeElement>("time[datetime]")) {
const date = new Date(el.dateTime);
if (!isNaN(date.getTime())) {
el.textContent = date.toLocaleDateString();
}
}
},
false
);

View File

@@ -62,6 +62,7 @@ body.type-webView {
#main {
max-width: unset;
padding: 0;
height: 100%;
}
#content {

View File

@@ -112,13 +112,13 @@ content = content.replaceAll(headingRe, (...match) => {
<body data-note-id="<%= note.noteId %>" class="type-<%= note.type %>" data-ancestor-note-id="<%= subRoot.note.noteId %>">
<%- renderSnippets("body:start") %>
<div id="header">
<button aria-label="Toggle Navigation" id="left-pane-toggle-button" class="header-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg></button>
<button aria-label="<%= t("share_theme.toggle-navigation") %>" id="left-pane-toggle-button" class="header-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg></button>
<a href="<%= shareRootLink %>" id="header-logo">
<img src="<%= logoUrl %>" width="32" height="<%= mobileLogoHeight %>" alt="Logo" />
<img src="<%= logoUrl %>" width="32" height="<%= mobileLogoHeight %>" alt="<%= t("share_theme.logo-alt") %>" />
<%= subRoot.note.title %>
</a>
<% if (headingMatches.length > 1) { %>
<button aria-label="Toggle Table of Contents" id="toc-pane-toggle-button" class="header-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10V7h2v2h-2zm0 6h2v-2h-2v2z"></path></svg></button>
<button aria-label="<%= t("share_theme.toggle-toc") %>" id="toc-pane-toggle-button" class="header-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10V7h2v2h-2zm0 6h2v-2h-2v2z"></path></svg></button>
<% } else { %>
<div class="header-button-placeholder"></div>
<% } %>
@@ -160,7 +160,7 @@ content = content.replaceAll(headingRe, (...match) => {
<%- renderSnippets("content:start") %>
<h1 id="title"><%= note.title %></h1>
<% if (isEmpty && (!note.hasVisibleChildren() && note.type !== "book")) { %>
<p>This note has no content.</p>
<p><%= t("share_page.no-content") %></p>
<% } else { %>
<%
content = content.replace(/<img /g, `<img alt="${t("share_theme.image_alt")}" loading="lazy" `);
@@ -196,7 +196,7 @@ content = content.replaceAll(headingRe, (...match) => {
<% if (!isEmpty && !isStatic) { %>
<div class="updated">
<% const lastUpdated = new Date(note.utcDateModified); %>
<%- t("share_theme.last-updated", { date: `<time datetime="${lastUpdated.toISOString()}">${lastUpdated.toLocaleDateString()}</time>`}) %>
<%- t("share_theme.last-updated", { date: `<time datetime="${lastUpdated.toISOString()}">${lastUpdated.toLocaleDateString("en-US")}</time>`}) %>
</div>
<% } %>

View File

@@ -316,6 +316,18 @@ class BNote extends AbstractBeccaEntity<BNote> {
return null;
}
/**
* Executes this note as a script. The note must be of type "Code: JS backend".
*
* @returns the return value of the executed script
*/
executeScript() {
// Lazy require to avoid circular dependency (script.ts imports BNote as a type).
// eslint-disable-next-line @typescript-eslint/no-require-imports
const scriptService = require("../../services/script.js").default;
return scriptService.executeNote(this, { originEntity: this });
}
/**
* Beware that the method must not create a copy of the array, but actually returns its internal array
* (for performance reasons)

View File

@@ -620,7 +620,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
}
const parentNoteId = opts.isVisible ? "_lbVisibleLaunchers" : "_lbAvailableLaunchers";
const noteId = `al_${ opts.id}`;
const noteId = `al_${opts.id}`;
const launcherNote =
becca.getNote(noteId) ||
@@ -698,7 +698,6 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
return `!@#Function: ${p.toString()}`;
}
return p;
});
}
};

View File

@@ -70,6 +70,11 @@ describe("processNoteContent", () => {
expect(content).toContain(`<img src="api/images/${bananaNote!.noteId}/banana.jpeg`);
});
it("can import ZIP with UTF-8 filenames without language encoding flag", async () => {
const { importedNote } = await testImport("utf8-filename.zip");
expect(importedNote.title).toBe("测试");
});
it("can import old geomap notes", async () => {
const { importedNote } = await testImport("geomap.zip");
expect(importedNote.type).toBe("book");