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>
This commit is contained in:
1 parent
313c41f867
commit
c0587cb267
29 files changed
+5274
-169
No files matched your search
@@ -0,0 +1,115 @@
|
||||
// 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');
|
||||
}
|
||||
Reference in new issue
Block a user