Unify Dayjs between client and server (#7930)

This commit is contained in:
Elian Doran 2025-12-04 07:08:11 +00:00 committed by GitHub
commit c081a596df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 344 additions and 157 deletions

View File

@ -38,8 +38,6 @@
"boxicons": "2.1.4", "boxicons": "2.1.4",
"clsx": "2.1.1", "clsx": "2.1.1",
"color": "5.0.3", "color": "5.0.3",
"dayjs": "1.11.19",
"dayjs-plugin-utc": "0.1.2",
"debounce": "3.0.0", "debounce": "3.0.0",
"draggabilly": "3.0.0", "draggabilly": "3.0.0",
"force-graph": "1.51.0", "force-graph": "1.51.0",

View File

@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { Bundle, executeBundle } from "./bundle";
import { buildNote } from "../test/easy-froca";
describe("Script bundle", () => {
it("dayjs is available", async () => {
const script = /* js */`return api.dayjs().format("YYYY-MM-DD");`;
const bundle = getBundle(script);
const result = await executeBundle(bundle, null, $());
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it("dayjs is-same-or-before plugin exists", async () => {
const script = /* js */`return api.dayjs("2023-10-01").isSameOrBefore(api.dayjs("2023-10-02"));`;
const bundle = getBundle(script);
const result = await executeBundle(bundle, null, $());
expect(result).toBe(true);
});
});
function getBundle(script: string) {
const id = buildNote({
title: "Script note"
}).noteId;
const bundle: Bundle = {
script: [
'',
`apiContext.modules['${id}'] = { exports: {} };`,
`return await ((async function(exports, module, require, api) {`,
`try {`,
`${script}`,
`;`,
`} catch (e) { throw new Error(\"Load of script note \\\"Client\\\" (${id}) failed with: \" + e.message); }`,
`for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];`,
`return module.exports;`,
`}).call({}, {}, apiContext.modules['${id}'], apiContext.require([]), apiContext.apis['${id}']));`,
''
].join('\n'),
html: "",
noteId: id,
allNoteIds: [ id ]
};
return bundle;
}

View File

@ -27,7 +27,7 @@ async function getAndExecuteBundle(noteId: string, originEntity = null, script =
return await executeBundle(bundle, originEntity); return await executeBundle(bundle, originEntity);
} }
async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) { export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container); const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container);
try { try {

View File

@ -1,4 +1,4 @@
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import type { FNoteRow } from "../entities/fnote.js"; import type { FNoteRow } from "../entities/fnote.js";
import froca from "./froca.js"; import froca from "./froca.js";
import server from "./server.js"; import server from "./server.js";

View File

@ -17,7 +17,7 @@ import shortcutService from "./shortcuts.js";
import dialogService from "./dialog.js"; import dialogService from "./dialog.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import type NoteContext from "../components/note_context.js"; import type NoteContext from "../components/note_context.js";
import type Component from "../components/component.js"; import type Component from "../components/component.js";
import { formatLogMessage } from "@triliumnext/commons"; import { formatLogMessage } from "@triliumnext/commons";

View File

@ -2,7 +2,7 @@ import options from "./options.js";
import i18next from "i18next"; import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend"; import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js"; import server from "./server.js";
import type { Locale } from "@triliumnext/commons"; import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
let locales: Locale[] | null; let locales: Locale[] | null;
@ -13,7 +13,7 @@ let locales: Locale[] | null;
export let translationsInitializedPromise = $.Deferred(); export let translationsInitializedPromise = $.Deferred();
export async function initLocale() { export async function initLocale() {
const locale = (options.get("locale") as string) || "en"; const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
locales = await server.get<Locale[]>("options/locales"); locales = await server.get<Locale[]>("options/locales");
@ -27,6 +27,7 @@ export async function initLocale() {
returnEmptyString: false returnEmptyString: false
}); });
await setDayjsLocale(locale);
translationsInitializedPromise.resolve(); translationsInitializedPromise.resolve();
} }

View File

@ -1,4 +1,4 @@
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import type { ViewScope } from "./link.js"; import type { ViewScope } from "./link.js";
import FNote from "../entities/fnote"; import FNote from "../entities/fnote";

View File

@ -46,6 +46,8 @@ function mockServer() {
attributes: [] attributes: []
} }
} }
console.warn(`Unsupported GET to mocked server: ${url}`);
}, },
async post(url: string, data: object) { async post(url: string, data: object) {

View File

@ -7,17 +7,10 @@ import toastService from "../../services/toast.js";
import options from "../../services/options.js"; import options from "../../services/options.js";
import { Dropdown } from "bootstrap"; import { Dropdown } from "bootstrap";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
import dayjs, { Dayjs } from "dayjs"; import { dayjs, type Dayjs } from "@triliumnext/commons";
import isoWeek from "dayjs/plugin/isoWeek.js";
import utc from "dayjs/plugin/utc.js";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
import "../../stylesheets/calendar.css"; import "../../stylesheets/calendar.css";
import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons"; import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons";
dayjs.extend(utc);
dayjs.extend(isSameOrAfter);
dayjs.extend(isoWeek);
const MONTHS = [ const MONTHS = [
t("calendar.january"), t("calendar.january"),
t("calendar.february"), t("calendar.february"),

View File

@ -2,7 +2,7 @@ import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js"; import server from "../services/server.js";
import fileWatcher from "../services/file_watcher.js"; import fileWatcher from "../services/file_watcher.js";
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import type { EventData } from "../components/app_context.js"; import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";

View File

@ -76,7 +76,6 @@
"compression": "1.8.1", "compression": "1.8.1",
"cookie-parser": "1.4.7", "cookie-parser": "1.4.7",
"csrf-csrf": "3.2.2", "csrf-csrf": "3.2.2",
"dayjs": "1.11.19",
"debounce": "3.0.0", "debounce": "3.0.0",
"debug": "4.4.3", "debug": "4.4.3",
"ejs": "3.1.10", "ejs": "3.1.10",

View File

@ -1,7 +1,7 @@
import { beforeAll } from "vitest"; import { beforeAll } from "vitest";
import i18next from "i18next"; import i18next from "i18next";
import { join } from "path"; import { join } from "path";
import dayjs from "dayjs"; import { setDayjsLocale } from "@triliumnext/commons";
// Initialize environment variables. // Initialize environment variables.
process.env.TRILIUM_DATA_DIR = join(__dirname, "db"); process.env.TRILIUM_DATA_DIR = join(__dirname, "db");
@ -25,6 +25,5 @@ beforeAll(async () => {
}); });
// Initialize dayjs // Initialize dayjs
await import("dayjs/locale/en.js"); await setDayjsLocale("en");
dayjs.locale("en");
}); });

File diff suppressed because one or more lines are too long

View File

@ -4,28 +4,29 @@ class="image image-style-align-center">
<img style="aspect-ratio:1398/1015;" src="Split View_2_Split View_im.png" <img style="aspect-ratio:1398/1015;" src="Split View_2_Split View_im.png"
width="1398" height="1015"> width="1398" height="1015">
</figure> </figure>
<h2><strong>Interactions</strong></h2>
<h2><strong>Interactions</strong></h2>
<ul> <ul>
<li data-list-item-id="eb22263532280510ca0efeb2c2e757629">Press the <li>Press the
<img src="Split View_Split View_imag.png">button to the right of a note's title to open a new split to the right <img src="Split View_Split View_imag.png">button to the right of a note's title to open a new split to the right
of it. of it.
<ul> <ul>
<li data-list-item-id="eda17492ea2d8da7c4bf2fb3e2f7bfbe9">It is possible to have as many splits as desired, simply press again the <li>It is possible to have as many splits as desired, simply press again the
button.</li> button.</li>
<li data-list-item-id="ea0223c947ea17534d577c9cfef4d5c6e">Only horizontal splits are possible, vertical or drag &amp; dropping is <li>Only horizontal splits are possible, vertical or drag &amp; dropping is
not supported.</li> not supported.</li>
</ul> </ul>
</li> </li>
<li data-list-item-id="e77d99fdc9a0846903de57d1b710fdd56">When at least one split is open, press the <li>When at least one split is open, press the
<img src="Split View_3_Split View_im.png">button next to it to close it.</li> <img src="Split View_3_Split View_im.png">button next to it to close it.</li>
<li data-list-item-id="ec9d11f5bcfd10795f282e275938b2f4a">Use the <li>Use the
<img src="Split View_4_Split View_im.png">or the <img src="Split View_4_Split View_im.png">or the
<img src="Split View_1_Split View_im.png">button to move around the splits.</li> <img src="Split View_1_Split View_im.png">button to move around the splits.</li>
<li data-list-item-id="e8384a579c3d6ee8df4d7dbf9b07c3436">Each <a href="#root/_help_3seOhtN8uLIY">tab</a> has its own split view configuration <li>Each <a href="#root/_help_3seOhtN8uLIY">tab</a> has its own split view configuration
(e.g. one tab can have two notes in a split view, whereas the others are (e.g. one tab can have two notes in a split view, whereas the others are
one-note views). one-note views).
<ul> <ul>
<li data-list-item-id="e298299f6b2f1b9d8b5f26a5f8a0c9092">The tab will indicate only the title of the main note (the first one in <li>The tab will indicate only the title of the main note (the first one in
the list).</li> the list).</li>
</ul> </ul>
</li> </li>
@ -48,35 +49,34 @@ class="image image-style-align-center">
as well, with the following differences from the desktop version of the as well, with the following differences from the desktop version of the
split:</p> split:</p>
<ul> <ul>
<li data-list-item-id="efc0bb8eb81ea1617b73188613f2ede5d">On smartphones, the split views are laid out vertically (one on the top <li>On smartphones, the split views are laid out vertically (one on the top
and one on the bottom), instead of horizontally as on the desktop.</li> and one on the bottom), instead of horizontally as on the desktop.</li>
<li <li>There can be only one split open per tab.</li>
data-list-item-id="e7659da22c36db39ae8e3dc5424afba1e">There can be only one split open per tab.</li> <li>It's not possible to resize the two split panes.</li>
<li data-list-item-id="eb848af9837a484a9de9117922c6d7186">It's not possible to resize the two split panes.</li> <li>When the keyboard is opened, the active note will be “maximized”, thus
<li data-list-item-id="e869c240066f602fbc1c0e55259ba62e5">When the keyboard is opened, the active note will be “maximized”, thus allowing for more space even when a split is open. When the keyboard is
allowing for more space even when a split is open. When the keyboard is closed, the splits become equal in size again.</li>
closed, the splits become equal in size again.</li>
</ul> </ul>
<p>Interaction:</p> <p>Interaction:</p>
<ul> <ul>
<li data-list-item-id="edbf5c644758db5aca9867c516f97542b">To create a new split, click the three dots button on the right of the <li>To create a new split, click the three dots button on the right of the
note title and select <em>Create new split</em>. note title and select <em>Create new split</em>.
<ul> <ul>
<li data-list-item-id="eed272873b629f70418c3e7074a829369">This option will only be available if there is no split already open in <li>This option will only be available if there is no split already open in
the current tab.</li> the current tab.</li>
</ul> </ul>
</li> </li>
<li data-list-item-id="e733863e6058336ebfcf27042f56be312">To close a split, click the three dots button on the right of the note <li>To close a split, click the three dots button on the right of the note
title and select <em>Close this pane</em>. title and select <em>Close this pane</em>.
<ul> <ul>
<li data-list-item-id="e9e6c191873dcd658242c64553343e0c7">Note that this option will only be available on the second note in the <li>Note that this option will only be available on the second note in the
split (the one at the bottom on smartphones, the one on the right on tablets).</li> split (the one at the bottom on smartphones, the one on the right on tablets).</li>
</ul> </ul>
</li> </li>
<li data-list-item-id="e780e5e7736b4d26705102545c5626a6a">When long-pressing a link, a contextual menu will show up with an option <li>When long-pressing a link, a contextual menu will show up with an option
to <em>Open note in a new split</em>. to <em>Open note in a new split</em>.
<ul> <ul>
<li data-list-item-id="e264277ee51f6d266a4088e2545f6648d">If there's already a split, the option will replace the existing split <li>If there's already a split, the option will replace the existing split
instead.</li> instead.</li>
</ul> </ul>
</li> </li>

View File

@ -0,0 +1,37 @@
<p>Day.js is a date manipulation library that's used by Trilium, but it's
also shared with both front-end and back-end scripts. For more information
about the library itself, consult the <a href="https://day.js.org/en/">official documentation</a>.</p>
<h2>How to use</h2>
<p>The <code>dayjs</code> method is provided directly in the <code>api</code> global:</p><pre><code class="language-application-javascript-env-backend">const date = api.dayjs();
api.log(date.format("YYYY-MM-DD"));</code></pre>
<h2>Plugins</h2>
<p>Day.js uses a modular, plugin-based architecture. Generally these plugins
must be imported, but this process doesn't work inside Trilium scripts
due to the use of a bundler.</p>
<p>Since v0.100.0, the same set of plugins is available for both front-end
and back-end scripts.</p>
<p>The following Day.js plugins are directly integrated into Trilium:</p>
<ul>
<li data-list-item-id="ee48062bdf09fc0c616bb530989292b21"><a href="https://day.js.org/docs/en/plugin/advanced-format">AdvancedFormat</a>
</li>
<li data-list-item-id="ed6e0cbd3a519a8720d1c9e9cc400bb04"><a href="https://day.js.org/docs/en/plugin/duration">Duration</a>, since
v0.100.0.</li>
<li data-list-item-id="e94f06e705bf337dd83b07f86d3f7adb7"><a href="https://day.js.org/docs/en/plugin/is-between">IsBetween</a>
</li>
<li data-list-item-id="e15e52376df80e3452567a47df67f41bd"><a href="https://day.js.org/docs/en/plugin/iso-week">IsoWeek</a>
</li>
<li data-list-item-id="e5c807c71a6b02320901809cc94dcca25"><a href="https://day.js.org/docs/en/plugin/is-same-or-after">IsSameOrAfter</a>
</li>
<li data-list-item-id="e1536f1bb4d9b9a40789ebe18a183e5d4"><a href="https://day.js.org/docs/en/plugin/is-same-or-before">IsSameOrBefore</a>
</li>
<li data-list-item-id="ec1f021ff7cf2edec5f8548843a1d5a0c"><a href="https://day.js.org/docs/en/plugin/quarter-of-year">QuarterOfYear</a>
</li>
<li data-list-item-id="e7cf8af04f2a71d8e380627cd6d3f6d04"><a href="https://day.js.org/docs/en/plugin/utc">UTC</a>
</li>
</ul>
<aside class="admonition note">
<p>If another Day.js plugin might be needed for scripting purposes, feel
free to open a feature request for it. Depending on the size of the plugin
and the potential use of it inside the Trilium code base, it has a chance
of being integrated.</p>
</aside>

View File

@ -11,8 +11,7 @@ import AbstractBeccaEntity from "./abstract_becca_entity.js";
import BRevision from "./brevision.js"; import BRevision from "./brevision.js";
import BAttachment from "./battachment.js"; import BAttachment from "./battachment.js";
import TaskContext from "../../services/task_context.js"; import TaskContext from "../../services/task_context.js";
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import utc from "dayjs/plugin/utc.js";
import eventService from "../../services/events.js"; import eventService from "../../services/events.js";
import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons"; import type { AttachmentRow, AttributeType, CloneResponse, NoteRow, NoteType, RevisionRow } from "@triliumnext/commons";
import type BBranch from "./bbranch.js"; import type BBranch from "./bbranch.js";
@ -22,7 +21,6 @@ import searchService from "../../services/search/services/search.js";
import cloningService from "../../services/cloning.js"; import cloningService from "../../services/cloning.js";
import noteService from "../../services/notes.js"; import noteService from "../../services/notes.js";
import handlers from "../../services/handlers.js"; import handlers from "../../services/handlers.js";
dayjs.extend(utc);
const LABEL = "label"; const LABEL = "label";
const RELATION = "relation"; const RELATION = "relation";

View File

@ -1,7 +1,7 @@
import { beforeAll, describe, expect, it } from "vitest"; import { beforeAll, describe, expect, it } from "vitest";
import supertest, { type Response } from "supertest"; import supertest, { type Response } from "supertest";
import type { Application } from "express"; import type { Application } from "express";
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import { type SQLiteSessionStore } from "./session_parser.js"; import { type SQLiteSessionStore } from "./session_parser.js";
import { SessionData } from "express-session"; import { SessionData } from "express-session";
import cls from "../services/cls.js"; import cls from "../services/cls.js";

View File

@ -7,7 +7,7 @@ import dateNoteService from "./date_notes.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import config from "./config.js"; import config from "./config.js";
import axios from "axios"; import axios from "axios";
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import xml2js from "xml2js"; import xml2js from "xml2js";
import * as cheerio from "cheerio"; import * as cheerio from "cheerio";
import cloningService from "./cloning.js"; import cloningService from "./cloning.js";
@ -37,17 +37,6 @@ import type Becca from "../becca/becca-interface.js";
import type { NoteParams } from "./note-interface.js"; import type { NoteParams } from "./note-interface.js";
import type { ApiParams } from "./backend_script_api_interface.js"; import type { ApiParams } from "./backend_script_api_interface.js";
// Dayjs plugins
import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import isBetween from "dayjs/plugin/isBetween";
import advancedFormat from "dayjs/plugin/advancedFormat.js";
dayjs.extend(isSameOrBefore);
dayjs.extend(isSameOrAfter);
dayjs.extend(isBetween);
dayjs.extend(advancedFormat);
/** /**
* A whole number * A whole number
* @typedef {number} int * @typedef {number} int

View File

@ -1,4 +1,4 @@
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import type BNote from "../becca/entities/bnote.js"; import type BNote from "../becca/entities/bnote.js";

View File

@ -1,27 +1,17 @@
import type BNote from "../becca/entities/bnote.js"; import type BNote from "../becca/entities/bnote.js";
import type { Dayjs } from "dayjs";
import advancedFormat from "dayjs/plugin/advancedFormat.js";
import attributeService from "./attributes.js"; import attributeService from "./attributes.js";
import cloningService from "./cloning.js"; import cloningService from "./cloning.js";
import dayjs from "dayjs"; import { dayjs, Dayjs } from "@triliumnext/commons";
import hoistedNoteService from "./hoisted_note.js"; import hoistedNoteService from "./hoisted_note.js";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
import noteService from "./notes.js"; import noteService from "./notes.js";
import optionService from "./options.js"; import optionService from "./options.js";
import protectedSessionService from "./protected_session.js"; import protectedSessionService from "./protected_session.js";
import quarterOfYear from "dayjs/plugin/quarterOfYear.js";
import searchContext from "../services/search/search_context.js"; import searchContext from "../services/search/search_context.js";
import searchService from "../services/search/services/search.js"; import searchService from "../services/search/services/search.js";
import sql from "./sql.js"; import sql from "./sql.js";
import { t } from "i18next"; import { t } from "i18next";
import { ordinal } from "./i18n.js"; import { ordinal } from "./i18n.js";
import isoWeek from "dayjs/plugin/isoWeek.js";
dayjs.extend(isSameOrAfter);
dayjs.extend(quarterOfYear);
dayjs.extend(advancedFormat);
dayjs.extend(isoWeek);
const CALENDAR_ROOT_LABEL = "calendarRoot"; const CALENDAR_ROOT_LABEL = "calendarRoot";
const YEAR_LABEL = "yearNote"; const YEAR_LABEL = "yearNote";

View File

@ -1,4 +1,4 @@
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import cls from "./cls.js"; import cls from "./cls.js";
const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ"; const LOCAL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss.SSSZZ";

View File

@ -1,7 +1,6 @@
import { LOCALES } from "@triliumnext/commons"; import { LOCALES } from "@triliumnext/commons";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { join } from "path"; import { join } from "path";
import { DAYJS_LOADER } from "./i18n";
describe("i18n", () => { describe("i18n", () => {
it("translations are valid JSON", () => { it("translations are valid JSON", () => {
@ -16,13 +15,4 @@ describe("i18n", () => {
.not.toThrow(); .not.toThrow();
} }
}); });
it("all dayjs locales are valid", async () => {
for (const locale of LOCALES) {
const dayjsLoader = DAYJS_LOADER[locale.id];
expect(dayjsLoader, `Locale ${locale.id} missing.`).toBeDefined();
await dayjsLoader();
}
});
}); });

View File

@ -4,31 +4,7 @@ import sql_init from "./sql_init.js";
import { join } from "path"; import { join } from "path";
import { getResourceDir } from "./utils.js"; import { getResourceDir } from "./utils.js";
import hidden_subtree from "./hidden_subtree.js"; import hidden_subtree from "./hidden_subtree.js";
import { LOCALES, type Locale, type LOCALE_IDS } from "@triliumnext/commons"; import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type Locale, type LOCALE_IDS } from "@triliumnext/commons";
import dayjs, { Dayjs } from "dayjs";
// When adding a new locale, prefer the version with hyphen instead of underscore.
export const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs/locale/en.js")>> = {
"ar": () => import("dayjs/locale/ar.js"),
"cn": () => import("dayjs/locale/zh-cn.js"),
"de": () => import("dayjs/locale/de.js"),
"en": () => import("dayjs/locale/en.js"),
"en-GB": () => import("dayjs/locale/en-gb.js"),
"en_rtl": () => import("dayjs/locale/en.js"),
"es": () => import("dayjs/locale/es.js"),
"fa": () => import("dayjs/locale/fa.js"),
"fr": () => import("dayjs/locale/fr.js"),
"it": () => import("dayjs/locale/it.js"),
"he": () => import("dayjs/locale/he.js"),
"ja": () => import("dayjs/locale/ja.js"),
"ku": () => import("dayjs/locale/ku.js"),
"pt_br": () => import("dayjs/locale/pt-br.js"),
"pt": () => import("dayjs/locale/pt.js"),
"ro": () => import("dayjs/locale/ro.js"),
"ru": () => import("dayjs/locale/ru.js"),
"tw": () => import("dayjs/locale/zh-tw.js"),
"uk": () => import("dayjs/locale/uk.js"),
}
export async function initializeTranslations() { export async function initializeTranslations() {
const resourceDir = getResourceDir(); const resourceDir = getResourceDir();
@ -46,10 +22,7 @@ export async function initializeTranslations() {
}); });
// Initialize dayjs locale. // Initialize dayjs locale.
const dayjsLocale = DAYJS_LOADER[locale]; await setDayjsLocale(locale);
if (dayjsLocale) {
dayjs.locale(await dayjsLocale());
}
} }
export function ordinal(date: Dayjs) { export function ordinal(date: Dayjs) {

View File

@ -1,4 +1,4 @@
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import sax from "sax"; import sax from "sax";
import stream from "stream"; import stream from "stream";
import { Throttle } from "stream-throttle"; import { Throttle } from "stream-throttle";

View File

@ -16,7 +16,7 @@ import BBranch from "../becca/entities/bbranch.js";
import BNote from "../becca/entities/bnote.js"; import BNote from "../becca/entities/bnote.js";
import BAttribute from "../becca/entities/battribute.js"; import BAttribute from "../becca/entities/battribute.js";
import BAttachment from "../becca/entities/battachment.js"; import BAttachment from "../becca/entities/battachment.js";
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import htmlSanitizer from "./html_sanitizer.js"; import htmlSanitizer from "./html_sanitizer.js";
import ValidationError from "../errors/validation_error.js"; import ValidationError from "../errors/validation_error.js";
import noteTypesService from "./note_types.js"; import noteTypesService from "./note_types.js";

View File

@ -59,7 +59,7 @@ describe("Script", () => {
}); });
}); });
describe("dayjs", () => { describe("dayjs in backend scripts", () => {
const scriptNote = note("dayjs", { const scriptNote = note("dayjs", {
type: "code", type: "code",
mime: "application/javascript;env=backend", mime: "application/javascript;env=backend",
@ -74,7 +74,7 @@ describe("Script", () => {
}); });
}); });
it("dayjs is-same-or-before", () => { it("dayjs is-same-or-before plugin exists", () => {
cls.init(() => { cls.init(() => {
const bundle = getScriptBundle(scriptNote.note, true, "backend", [], `return api.dayjs("2023-10-01").isSameOrBefore(api.dayjs("2023-10-02"));`); const bundle = getScriptBundle(scriptNote.note, true, "backend", [], `return api.dayjs("2023-10-01").isSameOrBefore(api.dayjs("2023-10-02"));`);
expect(bundle).toBeDefined(); expect(bundle).toBeDefined();
@ -82,33 +82,5 @@ describe("Script", () => {
expect(result).toBe(true); expect(result).toBe(true);
}); });
}); });
it("dayjs is-same-or-after", () => {
cls.init(() => {
const bundle = getScriptBundle(scriptNote.note, true, "backend", [], `return api.dayjs("2023-10-02").isSameOrAfter(api.dayjs("2023-10-01"));`);
expect(bundle).toBeDefined();
const result = executeBundle(bundle!);
expect(result).toBe(true);
});
});
it("dayjs is-between", () => {
cls.init(() => {
const bundle = getScriptBundle(scriptNote.note, true, "backend", [], `return api.dayjs("2023-10-02").isBetween(api.dayjs("2023-10-01"), api.dayjs("2023-10-03"));`);
expect(bundle).toBeDefined();
const result = executeBundle(bundle!);
expect(result).toBe(true);
});
});
// advanced format
it("dayjs advanced format", () => {
cls.init(() => {
const bundle = getScriptBundle(scriptNote.note, true, "backend", [], `return api.dayjs("2023-10-01").format("Q");`);
expect(bundle).toBeDefined();
const result = executeBundle(bundle!);
expect(result).not.toBe("Q");
});
});
}); });
}); });

View File

@ -1,6 +1,6 @@
"use strict"; "use strict";
import dayjs from "dayjs"; import { dayjs } from "@triliumnext/commons";
import AndExp from "../expressions/and.js"; import AndExp from "../expressions/and.js";
import OrExp from "../expressions/or.js"; import OrExp from "../expressions/or.js";
import NotExp from "../expressions/not.js"; import NotExp from "../expressions/not.js";

View File

@ -1,5 +1,5 @@
# Documentation # Documentation
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/KJUp1g3csedB/Documentation_image.png" width="205" height="162"> There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/rE2kRecw5n8A/Documentation_image.png" width="205" height="162">
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>. * The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers. * The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.

View File

@ -15928,6 +15928,41 @@
], ],
"dataFileName": "Backend API.dat", "dataFileName": "Backend API.dat",
"attachments": [] "attachments": []
},
{
"isClone": false,
"noteId": "ApVHZ8JY5ofC",
"notePath": [
"pOsGYCXsbNQG",
"CdNpE2pqjmI6",
"GLks18SNjxmC",
"ApVHZ8JY5ofC"
],
"title": "Day.js",
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "label",
"name": "shareAlias",
"value": "day.js",
"isInheritable": false,
"position": 30
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-calendar",
"isInheritable": false,
"position": 40
}
],
"format": "markdown",
"dataFileName": "Day.js.md",
"attachments": []
} }
] ]
}, },

View File

@ -0,0 +1,31 @@
# Day.js
Day.js is a date manipulation library that's used by Trilium, but it's also shared with both front-end and back-end scripts. For more information about the library itself, consult the [official documentation](https://day.js.org/en/).
## How to use
The `dayjs` method is provided directly in the `api` global:
```javascript
const date = api.dayjs();
api.log(date.format("YYYY-MM-DD"));
```
## Plugins
Day.js uses a modular, plugin-based architecture. Generally these plugins must be imported, but this process doesn't work inside Trilium scripts due to the use of a bundler.
Since v0.100.0, the same set of plugins is available for both front-end and back-end scripts.
The following Day.js plugins are directly integrated into Trilium:
* [AdvancedFormat](https://day.js.org/docs/en/plugin/advanced-format)
* [Duration](https://day.js.org/docs/en/plugin/duration), since v0.100.0.
* [IsBetween](https://day.js.org/docs/en/plugin/is-between)
* [IsoWeek](https://day.js.org/docs/en/plugin/iso-week)
* [IsSameOrAfter](https://day.js.org/docs/en/plugin/is-same-or-after)
* [IsSameOrBefore](https://day.js.org/docs/en/plugin/is-same-or-before)
* [QuarterOfYear](https://day.js.org/docs/en/plugin/quarter-of-year)
* [UTC](https://day.js.org/docs/en/plugin/utc)
> [!NOTE]
> If another Day.js plugin might be needed for scripting purposes, feel free to open a feature request for it. Depending on the size of the plugin and the potential use of it inside the Trilium code base, it has a chance of being integrated.

View File

@ -10,5 +10,12 @@
"name": "Trilium Notes Team", "name": "Trilium Notes Team",
"email": "contact@eliandoran.me", "email": "contact@eliandoran.me",
"url": "https://triliumnotes.org" "url": "https://triliumnotes.org"
},
"scripts": {
"test": "vitest"
},
"dependencies": {
"dayjs": "1.11.19",
"dayjs-plugin-utc": "0.1.2"
} }
} }

View File

@ -11,3 +11,4 @@ export * from "./lib/shared_constants.js";
export * from "./lib/ws_api.js"; export * from "./lib/ws_api.js";
export * from "./lib/attribute_names.js"; export * from "./lib/attribute_names.js";
export * from "./lib/utils.js"; export * from "./lib/utils.js";
export * from "./lib/dayjs.js";

View File

@ -0,0 +1,59 @@
/// <reference types="../../../../node_modules/dayjs/plugin/advancedFormat.d.ts" />
/// <reference types="../../../../node_modules/dayjs/plugin/duration.d.ts" />
/// <reference types="../../../../node_modules/dayjs/plugin/isBetween.d.ts" />
/// <reference types="../../../../node_modules/dayjs/plugin/isoWeek.d.ts" />
/// <reference types="../../../../node_modules/dayjs/plugin/isSameOrAfter.d.ts" />
/// <reference types="../../../../node_modules/dayjs/plugin/isSameOrBefore.d.ts" />
/// <reference types="../../../../node_modules/dayjs/plugin/quarterOfYear.d.ts" />
/// <reference types="../../../../node_modules/dayjs/plugin/utc.d.ts" />
import { LOCALES } from "./i18n.js";
import { DAYJS_LOADER, dayjs } from "./dayjs.js";
describe("dayjs", () => {
it("all dayjs locales are valid", async () => {
for (const locale of LOCALES) {
const dayjsLoader = DAYJS_LOADER[locale.id];
expect(dayjsLoader, `Locale ${locale.id} missing.`).toBeDefined();
await dayjsLoader();
}
});
describe("Plugins", () => {
it("advanced format is available", () => {
expect(dayjs("2023-10-01").format("Q")).not.toBe("Q");
});
it("duration plugin is available", () => {
const d = dayjs.duration({ hours: 2, minutes: 30 });
expect(d.asMinutes()).toBe(150);
});
it("is-between is available", () => {
expect(dayjs("2023-10-02").isBetween(dayjs("2023-10-01"), dayjs("2023-10-03"))).toBe(true);
});
it("iso-week is available", () => {
// ISO week number: 2023-01-01 is ISO week 52 of previous year
expect(dayjs("2023-01-01").isoWeek()).toBe(52);
});
it("is-same-or-before is available", () => {
expect(dayjs("2023-10-01").isSameOrBefore(dayjs("2023-10-02"))).toBe(true);
});
it("is-same-or-after is available", () => {
expect(dayjs("2023-10-02").isSameOrAfter(dayjs("2023-10-01"))).toBe(true);
});
it("quarter-year is available", () => {
expect(dayjs("2023-05-15").quarter()).toBe(2);
});
it("utc is available", () => {
const utcDate = dayjs("2023-10-01T12:00:00").utc();
expect(utcDate.utcOffset()).toBe(0);
});
});
});

View File

@ -0,0 +1,68 @@
import { default as dayjs, type Dayjs } from "dayjs";
import "dayjs/plugin/advancedFormat";
import "dayjs/plugin/duration";
import "dayjs/plugin/isBetween";
import "dayjs/plugin/isoWeek";
import "dayjs/plugin/isSameOrAfter";
import "dayjs/plugin/isSameOrBefore";
import "dayjs/plugin/quarterOfYear";
import "dayjs/plugin/utc";
//#region Plugins
import advancedFormat from "dayjs/plugin/advancedFormat.js";
import duration from "dayjs/plugin/duration.js";
import isBetween from "dayjs/plugin/isBetween.js";
import isoWeek from "dayjs/plugin/isoWeek.js";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js";
import quarterOfYear from "dayjs/plugin/quarterOfYear.js";
import utc from "dayjs/plugin/utc.js";
import { LOCALE_IDS } from "./i18n.js";
dayjs.extend(advancedFormat);
dayjs.extend(duration);
dayjs.extend(isBetween);
dayjs.extend(isoWeek);
dayjs.extend(isSameOrAfter);
dayjs.extend(isSameOrBefore);
dayjs.extend(quarterOfYear);
dayjs.extend(utc);
//#endregion
//#region Locales
export const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs/locale/en.js")>> = {
"ar": () => import("dayjs/locale/ar.js"),
"cn": () => import("dayjs/locale/zh-cn.js"),
"de": () => import("dayjs/locale/de.js"),
"en": () => import("dayjs/locale/en.js"),
"en-GB": () => import("dayjs/locale/en-gb.js"),
"en_rtl": () => import("dayjs/locale/en.js"),
"es": () => import("dayjs/locale/es.js"),
"fa": () => import("dayjs/locale/fa.js"),
"fr": () => import("dayjs/locale/fr.js"),
"it": () => import("dayjs/locale/it.js"),
"he": () => import("dayjs/locale/he.js"),
"ja": () => import("dayjs/locale/ja.js"),
"ku": () => import("dayjs/locale/ku.js"),
"pt_br": () => import("dayjs/locale/pt-br.js"),
"pt": () => import("dayjs/locale/pt.js"),
"ro": () => import("dayjs/locale/ro.js"),
"ru": () => import("dayjs/locale/ru.js"),
"tw": () => import("dayjs/locale/zh-tw.js"),
"uk": () => import("dayjs/locale/uk.js"),
}
async function setDayjsLocale(locale: LOCALE_IDS) {
const dayjsLocale = DAYJS_LOADER[locale];
if (dayjsLocale) {
dayjs.locale(await dayjsLocale());
}
}
//#endregion
export {
dayjs,
Dayjs,
setDayjsLocale
};

View File

@ -11,6 +11,7 @@ export interface Locale {
electronLocale?: "en" | "de" | "es" | "fr" | "zh_CN" | "zh_TW" | "ro" | "af" | "am" | "ar" | "bg" | "bn" | "ca" | "cs" | "da" | "el" | "en_GB" | "es_419" | "et" | "fa" | "fi" | "fil" | "gu" | "he" | "hi" | "hr" | "hu" | "id" | "it" | "ja" | "kn" | "ko" | "lt" | "lv" | "ml" | "mr" | "ms" | "nb" | "nl" | "pl" | "pt_BR" | "pt_PT" | "ru" | "sk" | "sl" | "sr" | "sv" | "sw" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "vi"; electronLocale?: "en" | "de" | "es" | "fr" | "zh_CN" | "zh_TW" | "ro" | "af" | "am" | "ar" | "bg" | "bn" | "ca" | "cs" | "da" | "el" | "en_GB" | "es_419" | "et" | "fa" | "fi" | "fil" | "gu" | "he" | "hi" | "hr" | "hu" | "id" | "it" | "ja" | "kn" | "ko" | "lt" | "lv" | "ml" | "mr" | "ms" | "nb" | "nl" | "pl" | "pt_BR" | "pt_PT" | "ru" | "sk" | "sl" | "sr" | "sv" | "sw" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "vi";
} }
// When adding a new locale, prefer the version with hyphen instead of underscore.
const UNSORTED_LOCALES = [ const UNSORTED_LOCALES = [
{ id: "cn", name: "简体中文", electronLocale: "zh_CN" }, { id: "cn", name: "简体中文", electronLocale: "zh_CN" },
{ id: "de", name: "Deutsch", electronLocale: "de" }, { id: "de", name: "Deutsch", electronLocale: "de" },

24
pnpm-lock.yaml generated
View File

@ -226,12 +226,6 @@ importers:
color: color:
specifier: 5.0.3 specifier: 5.0.3
version: 5.0.3 version: 5.0.3
dayjs:
specifier: 1.11.19
version: 1.11.19
dayjs-plugin-utc:
specifier: 0.1.2
version: 0.1.2
debounce: debounce:
specifier: 3.0.0 specifier: 3.0.0
version: 3.0.0 version: 3.0.0
@ -636,9 +630,6 @@ importers:
csrf-csrf: csrf-csrf:
specifier: 3.2.2 specifier: 3.2.2
version: 3.2.2 version: 3.2.2
dayjs:
specifier: 1.11.19
version: 1.11.19
debounce: debounce:
specifier: 3.0.0 specifier: 3.0.0
version: 3.0.0 version: 3.0.0
@ -1328,7 +1319,14 @@ importers:
specifier: 9.39.1 specifier: 9.39.1
version: 9.39.1 version: 9.39.1
packages/commons: {} packages/commons:
dependencies:
dayjs:
specifier: 1.11.19
version: 1.11.19
dayjs-plugin-utc:
specifier: 0.1.2
version: 0.1.2
packages/express-partial-content: packages/express-partial-content:
dependencies: dependencies:
@ -15150,6 +15148,8 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.2.0 '@ckeditor/ckeditor5-core': 47.2.0
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-code-block@47.2.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': '@ckeditor/ckeditor5-code-block@47.2.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)':
dependencies: dependencies:
@ -15214,8 +15214,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
'@ckeditor/ckeditor5-watchdog': 47.2.0 '@ckeditor/ckeditor5-watchdog': 47.2.0
es-toolkit: 1.39.5 es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)': '@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)':
dependencies: dependencies:
@ -15719,6 +15717,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.2.0 '@ckeditor/ckeditor5-utils': 47.2.0
'@ckeditor/ckeditor5-widget': 47.2.0 '@ckeditor/ckeditor5-widget': 47.2.0
ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41) ckeditor5: 47.2.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-mention@47.2.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': '@ckeditor/ckeditor5-mention@47.2.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)':
dependencies: dependencies: