Block editor, markdown round trip, formulas/databases/kanban/canvas models, graph simulation (time-budgeted ticks for big vaults), shortcut registry, dialog focus handling, HTML to Markdown for paste and clipping. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
116 lines
5.3 KiB
JavaScript
116 lines
5.3 KiB
JavaScript
// Settings beyond the few that have their own field on the store (theme, text size, ...). One object, saved as `prefs`.
|
|
// Every key has a default; cleanPrefs() fills gaps and drops anything of the wrong kind from a file written by hand.
|
|
export const DEFAULTS = {
|
|
// appearance
|
|
uiFont: 'inter', // 'inter' | 'system' | 'mono'
|
|
density: 'default', // sidebar rows and spacing: 'compact' | 'default' | 'comfortable'
|
|
sidebarWidth: 268, // px
|
|
showCover: true,
|
|
showIcon: true,
|
|
showBacklinks: true,
|
|
codeSize: 100, // % of the page's code size
|
|
codeWrap: false, // wrap long lines in code blocks instead of scrolling
|
|
customPageFont: '', // an installed font's name for page text; '' uses the Page font choice
|
|
customCodeFont: '', // same, for code
|
|
// editor
|
|
autoPair: true, // ( [ { ` close themselves
|
|
smartTypography: false, // -- to an em dash, ... to an ellipsis, curly quotes
|
|
pasteRich: true, // pasted web / rich text keeps headings, lists, links and bold as Markdown
|
|
mdShortcuts: true, // "# " and "- " turn a line into a heading or a list
|
|
typewriter: false, // keep the line being typed in the middle of the screen
|
|
hiddenKinds: [], // block kinds left out of the slash menu
|
|
autosaveDelay: 600, // ms after the last keystroke
|
|
// notes
|
|
dateFormat: 'YYYY-MM-DD', // daily note names
|
|
dailyTemplate: '', // text a new daily note starts with; {{date}} and {{weekday}} are filled in
|
|
newPageIcon: 'random', // 'random' | 'none'
|
|
newPageCover: 'random', // 'random' | 'none'
|
|
weekStart: 'monday', // calendar views: 'monday' | 'sunday'
|
|
locale: '', // date language; '' follows the system
|
|
exportLinks: 'wiki', // [[Page]] links in Markdown exports: 'wiki' | 'markdown'
|
|
confirmTrash: false, // ask before moving a page to the trash
|
|
trashDays: 30, // days in the trash before it is emptied; 0 never
|
|
historyKeep: 100, // versions kept per page
|
|
historyGapMin: 5, // at most one version per this many minutes of editing
|
|
// canvas
|
|
snapToGrid: true,
|
|
cardColor: '', // color of new canvas cards: '' (none) or a preset 1-6
|
|
// window
|
|
alwaysOnTop: false,
|
|
startMinimized: false,
|
|
// keyboard
|
|
shortcuts: {}, // { action id: 'Mod+Shift+K' } for the shortcuts that were changed (src/lib/shortcuts.js)
|
|
// quick capture
|
|
captureShortcut: 'CommandOrControl+Alt+N', // global shortcut for the capture window; '' turns it off
|
|
captureTo: 'daily', // 'daily' (today's note) | 'inbox'
|
|
// backups
|
|
backupEvery: 'off', // 'off' | 'daily' | 'weekly'
|
|
backupKeep: 5, // zips kept
|
|
backupFolder: '' // '' = <app data>/backups
|
|
};
|
|
|
|
export const DATE_FORMATS = ['YYYY-MM-DD', 'DD-MM-YYYY', 'MM-DD-YYYY', 'YYYY.MM.DD', 'YYYY-MM-DD dddd', 'MMMM D, YYYY'];
|
|
|
|
const CHOICES = {
|
|
uiFont: ['inter', 'system', 'mono'],
|
|
density: ['compact', 'default', 'comfortable'],
|
|
newPageIcon: ['random', 'none'],
|
|
newPageCover: ['random', 'none'],
|
|
weekStart: ['monday', 'sunday'],
|
|
exportLinks: ['wiki', 'markdown'],
|
|
backupEvery: ['off', 'daily', 'weekly'],
|
|
captureTo: ['daily', 'inbox'],
|
|
dateFormat: DATE_FORMATS,
|
|
cardColor: ['', '1', '2', '3', '4', '5', '6']
|
|
};
|
|
const RANGES = {
|
|
sidebarWidth: [220, 420], codeSize: [80, 140], autosaveDelay: [200, 5000],
|
|
trashDays: [0, 365], historyKeep: [5, 1000], historyGapMin: [1, 120], backupKeep: [1, 50]
|
|
};
|
|
|
|
export function cleanPrefs(raw) {
|
|
const src = raw && typeof raw === 'object' ? raw : {};
|
|
const out = {};
|
|
for (const [k, def] of Object.entries(DEFAULTS)) {
|
|
const v = src[k];
|
|
if (k === 'shortcuts') {
|
|
out[k] = {};
|
|
if (v && typeof v === 'object' && !Array.isArray(v)) for (const [id, c] of Object.entries(v)) if (typeof c === 'string' && /^Mod(\+(Alt|Shift))*\+.+$/.test(c)) out[k][id] = c;
|
|
continue;
|
|
}
|
|
if (CHOICES[k]) out[k] = CHOICES[k].includes(v) ? v : def;
|
|
else if (RANGES[k]) {
|
|
const n = Math.round(Number(v));
|
|
out[k] = Number.isFinite(n) && v !== '' && v !== null ? Math.min(RANGES[k][1], Math.max(RANGES[k][0], n)) : def;
|
|
} else if (typeof def === 'boolean') out[k] = typeof v === 'boolean' ? v : def;
|
|
else if (Array.isArray(def)) out[k] = Array.isArray(v) ? v.filter((x) => typeof x === 'string').slice(0, 64) : [];
|
|
else out[k] = typeof v === 'string' ? v.slice(0, 4000) : def;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// [[Page]] -> [Page](Page.md), [[Page|label]] -> [label](Page.md), ![[photo.png]] -> ; a #heading goes
|
|
// into the address. Links inside code are left alone.
|
|
export function wikiToMarkdownLinks(md) {
|
|
const enc = (t) => encodeURI(t).replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
const link = (embed, target, alias) => {
|
|
const [name, heading] = target.split('#');
|
|
const file = /\.[a-z0-9]{2,5}$/i.test(name) ? name : name + '.md';
|
|
const label = alias || target;
|
|
if (embed && /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(name)) return `})`;
|
|
return `${embed ? '!' : ''}[${label}](${enc(file)}${heading ? '#' + enc(heading.toLowerCase().replace(/\s+/g, '-')) : ''})`;
|
|
};
|
|
let fenced = false;
|
|
return md
|
|
.split('\n')
|
|
.map((line) => {
|
|
if (/^\s*(```|~~~)/.test(line)) { fenced = !fenced; return line; }
|
|
if (fenced) return line;
|
|
return line
|
|
.split(/(`[^`]*`)/)
|
|
.map((part, i) => (i % 2 ? part : part.replace(/(!?)\[\[([^\]|]+)(?:\|([^\]]*))?\]\]/g, (_, bang, target, alias) => link(!!bang, target.trim(), alias && alias.trim()))))
|
|
.join('');
|
|
})
|
|
.join('\n');
|
|
}
|