Files
ink-note-app/src/lib/prefs.js
T
Your NameandClaude Sonnet 5.5 c0587cb267 feat(app): editor engine, markdown, store and domain modules
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>
2026-10-15 11:57:09 +02:00

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]] -> ![](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 `![${alias || ''}](${enc(name)})`;
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');
}