domma-cms 0.54.2 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +151 -77
- package/README.md +4 -4
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +3 -3
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/menu-editor.html +19 -25
- package/admin/js/templates/plugin-code.html +1 -1
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +27 -97
- package/admin/js/templates/theme.html +173 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +25 -23
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/bin/cli.js +11 -2
- package/bin/lib/smtp-defaults.js +53 -0
- package/bin/update.js +13 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/theme.json +18 -0
- package/package.json +12 -4
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4453 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1002 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/analytics/admin/css/index.css +1 -0
- package/plugins/analytics/admin/templates/analytics.html +22 -13
- package/plugins/analytics/plugin.json +3 -0
- package/plugins/blog/admin/css/index.css +1 -0
- package/plugins/blog/admin/templates/blog.html +30 -18
- package/plugins/blog/admin/templates/categories.html +2 -2
- package/plugins/blog/admin/templates/comments.html +2 -2
- package/plugins/blog/admin/templates/post-editor.html +34 -34
- package/plugins/blog/admin/templates/settings.html +6 -3
- package/plugins/blog/admin/views/blog.js +8 -5
- package/plugins/blog/admin/views/categories.js +5 -10
- package/plugins/blog/admin/views/comments.js +5 -5
- package/plugins/blog/admin/views/post-editor.js +39 -20
- package/plugins/blog/admin/views/settings.js +52 -50
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +14 -13
- package/plugins/blog/plugin.js +36 -13
- package/plugins/blog/plugin.json +13 -5
- package/plugins/blog/plugin.public.js +312 -0
- package/plugins/contacts/admin/css/index.css +1 -0
- package/plugins/contacts/admin/templates/contacts.html +128 -0
- package/plugins/contacts/admin/views/contacts.js +237 -4
- package/plugins/contacts/collections/user-contacts/schema.json +108 -0
- package/plugins/contacts/plugin.js +214 -27
- package/plugins/contacts/plugin.json +4 -1
- package/plugins/invoice/admin/css/index.css +1 -0
- package/plugins/invoice/admin/templates/editor.html +140 -49
- package/plugins/invoice/admin/templates/index.html +153 -23
- package/plugins/invoice/admin/templates/issuers.html +2 -5
- package/plugins/invoice/admin/templates/receivers.html +2 -5
- package/plugins/invoice/admin/views/contacts-source.js +266 -0
- package/plugins/invoice/admin/views/editor.js +366 -199
- package/plugins/invoice/admin/views/export.js +199 -0
- package/plugins/invoice/admin/views/help-content.js +61 -0
- package/plugins/invoice/admin/views/index.js +582 -94
- package/plugins/invoice/admin/views/issuers.js +24 -17
- package/plugins/invoice/admin/views/media.js +172 -0
- package/plugins/invoice/admin/views/party-view.js +305 -67
- package/plugins/invoice/admin/views/payments.js +127 -0
- package/plugins/invoice/admin/views/print.js +130 -0
- package/plugins/invoice/admin/views/receivers.js +49 -16
- package/plugins/invoice/admin/views/send.js +212 -0
- package/plugins/invoice/admin/views/settings.js +594 -0
- package/plugins/invoice/admin/views/view-lifecycle.js +33 -0
- package/plugins/invoice/collections/invoice-issuers/schema.json +77 -11
- package/plugins/invoice/collections/invoice-receivers/schema.json +10 -9
- package/plugins/invoice/collections/invoices/schema.json +19 -13
- package/plugins/invoice/config.js +27 -6
- package/plugins/invoice/pdf.js +164 -0
- package/plugins/invoice/plugin.js +1217 -44
- package/plugins/invoice/plugin.json +10 -9
- package/plugins/invoice/templates/_base.css +1 -0
- package/plugins/invoice/templates/classic-nologo.html +100 -0
- package/plugins/invoice/templates/classic.html +91 -0
- package/plugins/invoice/templates/invoice-print.html +24 -0
- package/plugins/invoice/templates/minimal.html +99 -0
- package/plugins/invoice/templates/modern-nologo.html +114 -0
- package/plugins/invoice/templates/modern.html +113 -0
- package/plugins/invoice/templates/templates.json +11 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +33 -0
- package/plugins/notes/admin/views/notes.js +1 -1
- package/plugins/notes/plugin.json +2 -2
- package/plugins/surveys/lib/audience.js +37 -0
- package/plugins/surveys/lib/campaigns.js +43 -0
- package/plugins/surveys/lib/ledger.js +110 -0
- package/plugins/surveys/lib/sending.js +106 -0
- package/plugins/surveys/lib/stats.js +62 -0
- package/plugins/surveys/lib/submit.js +95 -0
- package/plugins/surveys/lib/tokens.js +28 -0
- package/plugins/surveys/plugin.public.js +149 -0
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +1 -1
- package/public/css/forms.css +1 -1
- package/public/css/menu-highlight.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/menu-decor.mjs +1 -1
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/scripts/build.js +37 -3
- package/scripts/copy-domma.js +48 -0
- package/scripts/seed.js +1996 -0
- package/scripts/setup.js +8 -0
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/forms.js +42 -3
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugins.js +50 -6
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +74 -0
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/email.js +29 -3
- package/server/services/health.js +23 -2
- package/server/services/markdown.js +70 -9
- package/server/services/menuRender.js +28 -4
- package/server/services/menus.js +25 -1
- package/server/services/permissionRegistry.js +24 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/plugins.js +229 -6
- package/server/services/renderer.js +148 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/themeSettings.js +541 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/todo/data/todos.json +0 -1
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Site Search Plugin - Admin Settings View
|
|
3
|
-
*/
|
|
4
|
-
import {apiRequest} from '/admin/js/api.js';
|
|
5
|
-
|
|
6
|
-
export const siteSearchView = {
|
|
7
|
-
templateUrl: '/plugins/site-search/admin/templates/site-search.html',
|
|
8
|
-
|
|
9
|
-
async onMount($container) {
|
|
10
|
-
let settings = {};
|
|
11
|
-
try {
|
|
12
|
-
settings = await apiRequest('/plugins/site-search/settings');
|
|
13
|
-
} catch {
|
|
14
|
-
E.toast('Could not load settings.', {type: 'error'});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ---- Basic settings ----
|
|
18
|
-
$container.find('#field-placeholder').val(settings.placeholder || 'Search pages...');
|
|
19
|
-
$container.find('#field-keyboard-shortcut').prop('checked', settings.keyboardShortcut !== false);
|
|
20
|
-
$container.find('#field-max-results').val(settings.maxResults ?? 10);
|
|
21
|
-
$container.find('#field-min-query-length').val(settings.minQueryLength ?? 2);
|
|
22
|
-
$container.find('#field-debounce-ms').val(settings.debounceMs ?? 300);
|
|
23
|
-
|
|
24
|
-
// ---- Position settings ----
|
|
25
|
-
const displayMode = settings.displayMode || 'navbar';
|
|
26
|
-
const fieldMode = document.getElementById('field-display-mode');
|
|
27
|
-
const fieldPos = document.getElementById('field-position');
|
|
28
|
-
const fieldOffX = document.getElementById('field-offset-x');
|
|
29
|
-
const fieldOffY = document.getElementById('field-offset-y');
|
|
30
|
-
const fieldIconOnly = document.getElementById('field-icon-only');
|
|
31
|
-
const posSection = document.getElementById('ss-pos-section');
|
|
32
|
-
const posGrid = document.getElementById('ss-pos-grid');
|
|
33
|
-
const modeBar = document.getElementById('ss-display-mode');
|
|
34
|
-
|
|
35
|
-
if (fieldMode) fieldMode.value = displayMode;
|
|
36
|
-
if (fieldPos) fieldPos.value = settings.position ?? 'bottom-right';
|
|
37
|
-
if (fieldOffX) fieldOffX.value = settings.offsetX ?? 32;
|
|
38
|
-
if (fieldOffY) fieldOffY.value = settings.offsetY ?? 32;
|
|
39
|
-
if (fieldIconOnly) fieldIconOnly.checked = settings.iconOnly !== false;
|
|
40
|
-
|
|
41
|
-
function applyMode(mode) {
|
|
42
|
-
if (fieldMode) fieldMode.value = mode;
|
|
43
|
-
if (posSection) posSection.style.display = mode === 'floating' ? '' : 'none';
|
|
44
|
-
if (modeBar) {
|
|
45
|
-
modeBar.querySelectorAll('.ss-mode-btn').forEach(function (btn) {
|
|
46
|
-
const active = btn.getAttribute('data-mode') === mode;
|
|
47
|
-
btn.classList.toggle('btn-primary', active);
|
|
48
|
-
btn.classList.toggle('btn-secondary', !active);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function updatePosBtns(pos) {
|
|
54
|
-
document.querySelectorAll('.ss-pos-btn').forEach(function (btn) {
|
|
55
|
-
const active = btn.getAttribute('data-pos') === pos;
|
|
56
|
-
btn.style.background = active ? 'var(--dm-primary, #5b8cff)' : '';
|
|
57
|
-
btn.style.color = active ? '#fff' : '';
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
applyMode(displayMode);
|
|
62
|
-
updatePosBtns(settings.position || 'bottom-right');
|
|
63
|
-
|
|
64
|
-
// Mode toggle buttons
|
|
65
|
-
if (modeBar) {
|
|
66
|
-
modeBar.addEventListener('click', function (e) {
|
|
67
|
-
const btn = e.target.closest('.ss-mode-btn');
|
|
68
|
-
if (!btn) return;
|
|
69
|
-
applyMode(btn.getAttribute('data-mode'));
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Position grid
|
|
74
|
-
if (posGrid) {
|
|
75
|
-
posGrid.addEventListener('click', function (e) {
|
|
76
|
-
const btn = e.target.closest('[data-pos]');
|
|
77
|
-
if (!btn) return;
|
|
78
|
-
const pos = btn.getAttribute('data-pos');
|
|
79
|
-
if (fieldPos) fieldPos.value = pos;
|
|
80
|
-
updatePosBtns(pos);
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ---- Save ----
|
|
85
|
-
$container.find('#save-settings-btn').off('click').on('click', async () => {
|
|
86
|
-
const data = {
|
|
87
|
-
placeholder: $container.find('#field-placeholder').val() || 'Search pages...',
|
|
88
|
-
keyboardShortcut: $container.find('#field-keyboard-shortcut').prop('checked'),
|
|
89
|
-
maxResults: parseInt($container.find('#field-max-results').val(), 10) || 10,
|
|
90
|
-
minQueryLength: parseInt($container.find('#field-min-query-length').val(), 10) || 2,
|
|
91
|
-
debounceMs: parseInt($container.find('#field-debounce-ms').val(), 10) || 300,
|
|
92
|
-
displayMode: fieldMode ? fieldMode.value : 'navbar',
|
|
93
|
-
position: fieldPos ? fieldPos.value : 'bottom-right',
|
|
94
|
-
offsetX: fieldOffX ? (parseInt(fieldOffX.value, 10) || 32) : 32,
|
|
95
|
-
offsetY: fieldOffY ? (parseInt(fieldOffY.value, 10) || 32) : 32,
|
|
96
|
-
iconOnly: fieldIconOnly ? fieldIconOnly.checked : true
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
await apiRequest('/plugins/site-search/settings', {
|
|
101
|
-
method: 'PUT',
|
|
102
|
-
body: JSON.stringify(data)
|
|
103
|
-
});
|
|
104
|
-
E.toast('Settings saved.', {type: 'success'});
|
|
105
|
-
} catch {
|
|
106
|
-
E.toast('Failed to save settings.', {type: 'error'});
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Initialise tabs
|
|
111
|
-
const tabsEl = $container.find('#ss-settings-tabs').get(0);
|
|
112
|
-
if (tabsEl) E.tabs(tabsEl);
|
|
113
|
-
|
|
114
|
-
Domma.icons.scan();
|
|
115
|
-
}
|
|
116
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Site Search Plugin - Default Configuration
|
|
3
|
-
*/
|
|
4
|
-
export default {
|
|
5
|
-
placeholder: 'Search pages...',
|
|
6
|
-
keyboardShortcut: true,
|
|
7
|
-
maxResults: 10,
|
|
8
|
-
minQueryLength: 2,
|
|
9
|
-
debounceMs: 300,
|
|
10
|
-
displayMode: 'navbar',
|
|
11
|
-
position: 'bottom-right',
|
|
12
|
-
offsetX: 32,
|
|
13
|
-
offsetY: 32,
|
|
14
|
-
iconOnly: true
|
|
15
|
-
};
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Site Search Plugin - Server
|
|
3
|
-
*
|
|
4
|
-
* Endpoints (prefix: /api/plugins/site-search):
|
|
5
|
-
* GET /search?q=term - public: full-text search across all published pages
|
|
6
|
-
* GET /settings - public: return plugin settings (used by inject script)
|
|
7
|
-
* PUT /settings - admin-auth: save user overrides
|
|
8
|
-
*/
|
|
9
|
-
import {listPages} from '../../server/services/content.js';
|
|
10
|
-
import {getPluginSettings, savePluginState} from '../../server/services/plugins.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Strip markdown syntax and shortcodes from content for plain-text searching.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} content
|
|
16
|
-
* @returns {string}
|
|
17
|
-
*/
|
|
18
|
-
function stripMarkdown(content) {
|
|
19
|
-
if (!content) return '';
|
|
20
|
-
return content
|
|
21
|
-
// Remove shortcodes (self-closing and wrapping)
|
|
22
|
-
.replace(/\[[a-z][^\]]*\/\]/gi, '')
|
|
23
|
-
.replace(/\[[a-z][^\]]*\][\s\S]*?\[\/[a-z]+\]/gi, '')
|
|
24
|
-
.replace(/\[[a-z][^\]]*\]/gi, '')
|
|
25
|
-
// Remove HTML tags
|
|
26
|
-
.replace(/<[^>]+>/g, '')
|
|
27
|
-
// Remove headings (keep text)
|
|
28
|
-
.replace(/^#{1,6}\s+/gm, '')
|
|
29
|
-
// Remove bold / italic
|
|
30
|
-
.replace(/\*{1,3}([^*]+)\*{1,3}/g, '$1')
|
|
31
|
-
.replace(/_{1,3}([^_]+)_{1,3}/g, '$1')
|
|
32
|
-
// Remove images
|
|
33
|
-
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
34
|
-
// Remove links (keep text)
|
|
35
|
-
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
36
|
-
// Remove code blocks
|
|
37
|
-
.replace(/```[\s\S]*?```/g, '')
|
|
38
|
-
.replace(/`[^`]+`/g, '')
|
|
39
|
-
// Remove blockquotes marker
|
|
40
|
-
.replace(/^>\s+/gm, '')
|
|
41
|
-
// Collapse whitespace
|
|
42
|
-
.replace(/\s+/g, ' ')
|
|
43
|
-
.trim();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Build a snippet of ~length chars around the first occurrence of term in content.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} rawContent - raw markdown content
|
|
50
|
-
* @param {string} term
|
|
51
|
-
* @param {number} length
|
|
52
|
-
* @returns {string}
|
|
53
|
-
*/
|
|
54
|
-
function buildSnippet(rawContent, term, length = 120) {
|
|
55
|
-
const text = stripMarkdown(rawContent);
|
|
56
|
-
if (!text) return '';
|
|
57
|
-
|
|
58
|
-
const lower = text.toLowerCase();
|
|
59
|
-
const idx = lower.indexOf(term.toLowerCase());
|
|
60
|
-
|
|
61
|
-
if (idx === -1) {
|
|
62
|
-
return text.length > length ? text.slice(0, length) + '…' : text;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const half = Math.floor(length / 2);
|
|
66
|
-
const start = Math.max(0, idx - half);
|
|
67
|
-
const end = Math.min(text.length, start + length);
|
|
68
|
-
const snippet = text.slice(start, end);
|
|
69
|
-
|
|
70
|
-
return (start > 0 ? '…' : '') + snippet + (end < text.length ? '…' : '');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Score a page against a search query using weighted field matching.
|
|
75
|
-
*
|
|
76
|
-
* @param {object} page
|
|
77
|
-
* @param {string} query
|
|
78
|
-
* @param {string[]} words - query split into words
|
|
79
|
-
* @returns {number}
|
|
80
|
-
*/
|
|
81
|
-
function scorePage(page, query, words) {
|
|
82
|
-
let score = 0;
|
|
83
|
-
const title = (page.title || '').toLowerCase();
|
|
84
|
-
const description = (page.description || '').toLowerCase();
|
|
85
|
-
const tagsRaw = Array.isArray(page.tags) ? page.tags : [];
|
|
86
|
-
const tags = tagsRaw.map(t => t.toLowerCase());
|
|
87
|
-
const stripped = stripMarkdown(page.content || '').toLowerCase();
|
|
88
|
-
const q = query.toLowerCase();
|
|
89
|
-
|
|
90
|
-
// Title scoring
|
|
91
|
-
if (title === q) {
|
|
92
|
-
score += 100;
|
|
93
|
-
} else if (title.includes(q)) {
|
|
94
|
-
score += 60;
|
|
95
|
-
} else if (words.every(w => title.includes(w))) {
|
|
96
|
-
score += 40;
|
|
97
|
-
} else if (words.some(w => title.includes(w))) {
|
|
98
|
-
score += 20;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Tags scoring
|
|
102
|
-
if (tags.includes(q)) {
|
|
103
|
-
score += 30;
|
|
104
|
-
} else if (tags.some(t => t.includes(q))) {
|
|
105
|
-
score += 20;
|
|
106
|
-
} else if (words.every(w => tags.some(t => t.includes(w)))) {
|
|
107
|
-
score += 15;
|
|
108
|
-
} else if (words.some(w => tags.some(t => t.includes(w)))) {
|
|
109
|
-
score += 8;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Description scoring
|
|
113
|
-
if (description.includes(q)) {
|
|
114
|
-
score += 25;
|
|
115
|
-
} else if (words.some(w => description.includes(w))) {
|
|
116
|
-
score += 10;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Content scoring
|
|
120
|
-
if (stripped.includes(q)) {
|
|
121
|
-
score += 10;
|
|
122
|
-
} else if (words.every(w => stripped.includes(w))) {
|
|
123
|
-
score += 5;
|
|
124
|
-
} else if (words.some(w => stripped.includes(w))) {
|
|
125
|
-
score += 2;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return score;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export default async function siteSearchPlugin(fastify, options) {
|
|
132
|
-
const {authenticate, requireAdmin} = options.auth;
|
|
133
|
-
|
|
134
|
-
// -------------------------------------------------------------------------
|
|
135
|
-
// Public search endpoint
|
|
136
|
-
// -------------------------------------------------------------------------
|
|
137
|
-
|
|
138
|
-
fastify.get('/search', async (request, reply) => {
|
|
139
|
-
const settings = getPluginSettings('site-search');
|
|
140
|
-
const minLen = settings.minQueryLength ?? 2;
|
|
141
|
-
const maxResults = settings.maxResults ?? 10;
|
|
142
|
-
|
|
143
|
-
const q = (request.query.q || '').trim();
|
|
144
|
-
if (q.length < minLen) {
|
|
145
|
-
return reply.status(400).send({error: `Query must be at least ${minLen} characters`});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const pages = await listPages();
|
|
149
|
-
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
150
|
-
|
|
151
|
-
const results = [];
|
|
152
|
-
for (const page of pages) {
|
|
153
|
-
// Filter out drafts and private pages
|
|
154
|
-
if (page.status === 'draft') continue;
|
|
155
|
-
if (page.visibility === 'private') continue;
|
|
156
|
-
|
|
157
|
-
const score = scorePage(page, q, words);
|
|
158
|
-
if (score === 0) continue;
|
|
159
|
-
|
|
160
|
-
results.push({
|
|
161
|
-
title: page.title || 'Untitled',
|
|
162
|
-
url: page.urlPath,
|
|
163
|
-
description: page.description || '',
|
|
164
|
-
tags: Array.isArray(page.tags) ? page.tags : [],
|
|
165
|
-
snippet: buildSnippet(page.content || '', q),
|
|
166
|
-
score
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
results.sort((a, b) => b.score - a.score);
|
|
171
|
-
|
|
172
|
-
return {results: results.slice(0, maxResults)};
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// -------------------------------------------------------------------------
|
|
176
|
-
// Settings routes
|
|
177
|
-
// -------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
fastify.get('/settings', async () => {
|
|
180
|
-
return getPluginSettings('site-search');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
fastify.put('/settings', {preHandler: [authenticate, requireAdmin]}, async (request) => {
|
|
184
|
-
const body = request.body || {};
|
|
185
|
-
savePluginState('site-search', {settings: body});
|
|
186
|
-
return {ok: true};
|
|
187
|
-
});
|
|
188
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "site-search",
|
|
3
|
-
"displayName": "Site Search",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "Full-text search for the public site. Search icon in navbar, Cmd+K shortcut, weighted results.",
|
|
6
|
-
"author": "Darryl Waterhouse",
|
|
7
|
-
"date": "2026-03-19",
|
|
8
|
-
"icon": "search",
|
|
9
|
-
"admin": {
|
|
10
|
-
"sidebar": [
|
|
11
|
-
{
|
|
12
|
-
"id": "site-search",
|
|
13
|
-
"text": "Site Search",
|
|
14
|
-
"icon": "search",
|
|
15
|
-
"url": "#/plugins/site-search",
|
|
16
|
-
"section": "#/plugins/site-search",
|
|
17
|
-
"permissions": [
|
|
18
|
-
"settings"
|
|
19
|
-
]
|
|
20
|
-
}
|
|
21
|
-
],
|
|
22
|
-
"routes": [
|
|
23
|
-
{
|
|
24
|
-
"path": "/plugins/site-search",
|
|
25
|
-
"view": "plugin-site-search",
|
|
26
|
-
"title": "Site Search - Domma CMS"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"views": {
|
|
30
|
-
"plugin-site-search": {
|
|
31
|
-
"entry": "site-search/admin/views/site-search.js?v=0c7dd970",
|
|
32
|
-
"exportName": "siteSearchView"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"inject": {
|
|
37
|
-
"headLate": "public/inject-head.html",
|
|
38
|
-
"bodyEnd": "public/inject-body.html"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<!-- site-search: load settings then initialise search UI -->
|
|
2
|
-
<script>
|
|
3
|
-
(function () {
|
|
4
|
-
fetch('/api/plugins/site-search/settings')
|
|
5
|
-
.then(function (r) {
|
|
6
|
-
return r.json();
|
|
7
|
-
})
|
|
8
|
-
.then(function (cfg) {
|
|
9
|
-
window.__SITE_SEARCH__ = cfg;
|
|
10
|
-
var s = document.createElement('script');
|
|
11
|
-
s.src = '/plugins/site-search/public/search.js';
|
|
12
|
-
document.body.appendChild(s);
|
|
13
|
-
})
|
|
14
|
-
.catch(function () {
|
|
15
|
-
});
|
|
16
|
-
}());
|
|
17
|
-
</script>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<link rel="stylesheet" href="/plugins/site-search/public/search.css?v=2">
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.site-search-trigger{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;cursor:pointer;padding:6px 8px;border-radius:var(--dm-radius, 6px);color:inherit;opacity:.75;transition:opacity .15s,background .15s;font-size:var(--dm-font-size-sm, 14px)}.site-search-trigger:hover{opacity:1;background:var(--dm-surface-hover, rgba(0, 0, 0, .06))}.site-search-trigger svg,.site-search-trigger span[data-icon]{width:18px;height:18px}.site-search-shortcut-hint{display:inline-block;font-size:11px;line-height:1;padding:2px 5px;border-radius:4px;border:1px solid var(--dm-border, rgba(0, 0, 0, .15));color:var(--dm-text-muted, #888);margin-left:4px;vertical-align:middle;font-family:var(--dm-font-mono, monospace)}.site-search-overlay{position:fixed;inset:0;z-index:10000;background:#00000073;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:flex;align-items:flex-start;justify-content:center;padding-top:10vh;animation:ss-overlay-in .15s ease}@keyframes ss-overlay-in{0%{opacity:0}to{opacity:1}}.site-search-panel{width:100%;max-width:600px;margin:0 16px;border-radius:var(--dm-radius-lg, 10px);background:#1e2030;color:#e8eaf0;border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 64px #0009;overflow:hidden;animation:ss-panel-in .15s ease}@keyframes ss-panel-in{0%{transform:translateY(-12px);opacity:0}to{transform:translateY(0);opacity:1}}.site-search-header{display:flex;align-items:center;padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.08);gap:8px}.site-search-header span[data-icon],.site-search-header svg{width:18px;height:18px;flex-shrink:0;opacity:.45;color:#e8eaf0}.site-search-input{flex:1;border:none;outline:none;background:transparent;font-size:var(--dm-font-size-lg, 16px);color:#e8eaf0;font-family:var(--dm-font-sans, sans-serif);padding:0}.site-search-input::placeholder{color:#e8eaf066;opacity:1}.site-search-close-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid rgba(255,255,255,.15);cursor:pointer;color:#e8eaf080;border-radius:var(--dm-radius, 6px);padding:4px 8px;font-size:12px;font-family:var(--dm-font-mono, monospace);transition:color .15s,border-color .15s;flex-shrink:0}.site-search-close-btn:hover{color:#e8eaf0;border-color:#ffffff4d}.site-search-results{max-height:60vh;overflow-y:auto;padding:8px 0}.site-search-result{display:block;padding:10px 16px;text-decoration:none;color:inherit;border-left:3px solid transparent;transition:background .1s,border-color .1s;cursor:pointer}.site-search-result:hover,.site-search-result.active{background:#ffffff0f;border-left-color:var(--dm-primary, #5b8cff)}.site-search-result-title{font-weight:600;font-size:var(--dm-font-size-sm, 14px);margin-bottom:2px;color:#e8eaf0}.site-search-result-snippet{font-size:12px;color:#e8eaf08c;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.site-search-result-snippet mark{background:#5b8cff40;color:#e8eaf0;border-radius:2px;padding:0 1px}.site-search-loading,.site-search-empty{padding:24px 16px;text-align:center;color:#e8eaf073;font-size:var(--dm-font-size-sm, 14px)}.site-search-loading-dots{display:inline-flex;gap:4px}.site-search-loading-dots span{width:6px;height:6px;border-radius:50%;background:#e8eaf066;animation:ss-dot-pulse 1.2s ease-in-out infinite}.site-search-loading-dots span:nth-child(2){animation-delay:.2s}.site-search-loading-dots span:nth-child(3){animation-delay:.4s}@keyframes ss-dot-pulse{0%,80%,to{transform:scale(.6);opacity:.4}40%{transform:scale(1);opacity:1}}.site-search-results:not(:empty){border-top:1px solid rgba(255,255,255,.06)}.site-search-footer{padding:6px 16px;border-top:1px solid rgba(255,255,255,.06);display:flex;gap:12px;font-size:11px;color:#e8eaf059}.site-search-footer kbd{display:inline-block;padding:1px 4px;border-radius:3px;border:1px solid rgba(255,255,255,.15);font-family:var(--dm-font-mono, monospace);font-size:10px}@media(max-width:640px){.site-search-overlay{padding-top:5vh;align-items:flex-start}.site-search-panel{margin:0 8px;border-radius:var(--dm-radius, 6px)}.site-search-results{max-height:70vh}.site-search-shortcut-hint{display:none}}.site-search-floating-trigger{position:fixed;z-index:9999;width:52px;height:52px;border-radius:50%;border:1px solid var(--dm-border, rgba(0, 0, 0, .12));cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;background:var(--dm-surface, #fff);color:var(--dm-text, #111);box-shadow:0 4px 16px #00000024;transition:background .15s,color .15s,border-color .15s,transform .15s,box-shadow .15s;outline:none;white-space:nowrap}.site-search-floating-trigger:hover{background:var(--dm-primary, #5b8cff);color:#fff;border-color:var(--dm-primary, #5b8cff);box-shadow:0 6px 20px #00000038;transform:scale(1.05)}.site-search-floating-trigger:active{transform:scale(.97)}.site-search-floating-trigger svg,.site-search-floating-trigger [data-icon]{width:20px;height:20px;flex-shrink:0;pointer-events:none}.site-search-floating-trigger--pill{width:auto;height:44px;border-radius:999px;padding:0 18px 0 14px}.site-search-floating-label{font-size:.875rem;font-weight:500;letter-spacing:.01em;pointer-events:none}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(function(){"use strict";var l=window.__SITE_SEARCH__||{},N=l.placeholder||"Search pages...",g=l.keyboardShortcut!==!1,w=l.debounceMs||300,D=l.displayMode||"navbar",y=l.iconOnly!==!1,u=null,s=null,c=null,p=null,f=!1;function h(){var t=document.querySelector("#site-navbar, .navbar");if(t){var e=t.querySelector(".navbar-actions");if(!e){var n=t.querySelector(".navbar-container")||t;e=document.createElement("div"),e.className="navbar-actions",n.appendChild(e)}if(!e.querySelector(".site-search-trigger")){var r=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),o=r?"\u2318K":"Ctrl+K",a=document.createElement("button");a.className="navbar-action-link site-search-trigger",a.setAttribute("aria-label","Search"),a.setAttribute("type","button");var i=document.createElement("span");if(i.setAttribute("data-icon","search"),a.appendChild(i),g){var d=document.createElement("span");d.className="site-search-shortcut-hint",d.textContent=o,a.appendChild(d)}e.insertBefore(a,e.firstChild),a.addEventListener("click",b),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(a)}}}function L(){if(document.querySelector("#site-navbar .navbar-actions, .navbar .navbar-actions")){h();return}var t=new MutationObserver(function(){var e=document.querySelector("#site-navbar, .navbar");e&&(t.disconnect(),h())});t.observe(document.body,{childList:!0,subtree:!0}),setTimeout(function(){t.disconnect(),h()},5e3)}function k(){var t=document.createElement("div");t.className="site-search-overlay",t.setAttribute("role","dialog"),t.setAttribute("aria-modal","true"),t.setAttribute("aria-label","Site search");var e=document.createElement("div");e.className="site-search-panel",e.setAttribute("role","search");var n=document.createElement("div");n.className="site-search-header";var r=document.createElement("span");r.setAttribute("data-icon","search"),n.appendChild(r),s=document.createElement("input"),s.className="site-search-input",s.type="search",s.setAttribute("autocomplete","off"),s.setAttribute("autocorrect","off"),s.setAttribute("spellcheck","false"),s.setAttribute("aria-label","Search"),s.placeholder=N,n.appendChild(s);var o=document.createElement("button");o.className="site-search-close-btn",o.setAttribute("type","button"),o.setAttribute("aria-label","Close search"),o.textContent="Esc",n.appendChild(o),e.appendChild(n),c=document.createElement("div"),c.className="site-search-results",c.setAttribute("role","listbox"),c.setAttribute("aria-live","polite"),e.appendChild(c);var a=document.createElement("div");return a.className="site-search-footer",a.innerHTML="<span><kbd>\u2191</kbd><kbd>\u2193</kbd> navigate</span><span><kbd>\u21B5</kbd> open</span><span><kbd>Esc</kbd> close</span>",e.appendChild(a),t.appendChild(e),t.addEventListener("click",function(i){i.target===t&&v()}),o.addEventListener("click",v),s.addEventListener("input",function(){clearTimeout(p);var i=s.value.trim();if(!i){E();return}p=setTimeout(function(){T(i)},w)}),s.addEventListener("keydown",q),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(n),t}function b(){f||(f=!0,u=k(),document.body.appendChild(u),document.body.style.overflow="hidden",setTimeout(function(){s&&s.focus()},30))}function v(){f&&(f=!1,clearTimeout(p),u&&u.parentNode&&u.parentNode.removeChild(u),u=null,s=null,c=null,document.body.style.overflow="")}function T(t){c&&(x(),fetch("/api/plugins/site-search/search?q="+encodeURIComponent(t)).then(function(e){return e.json()}).then(function(e){M(e.results||[],t)}).catch(function(){E();var e=document.createElement("div");e.className="site-search-empty",e.textContent="Search unavailable. Please try again.",c.appendChild(e)}))}function x(){c&&(c.innerHTML='<div class="site-search-loading"><div class="site-search-loading-dots"><span></span><span></span><span></span></div></div>')}function E(){c&&(c.innerHTML="")}function M(t,e){if(c){if(c.innerHTML="",!t.length){var n=document.createElement("div");n.className="site-search-empty",n.textContent='No results found for "'+e+'"',c.appendChild(n);return}for(var r=document.createDocumentFragment(),o=0;o<t.length;o++){var a=t[o],i=document.createElement("a");i.className="site-search-result",i.href=a.url||"/",i.setAttribute("role","option"),i.setAttribute("tabindex","-1"),i.setAttribute("data-index",String(o));var d=document.createElement("div");if(d.className="site-search-result-title",C(d,a.title||"Untitled",e),i.appendChild(d),a.snippet){var m=document.createElement("div");m.className="site-search-result-snippet",C(m,a.snippet,e),i.appendChild(m)}i.addEventListener("click",v),r.appendChild(i)}c.appendChild(r)}}function C(t,e,n){if(!n){t.textContent=e;return}var r=n.trim().split(/\s+/).filter(Boolean);if(!r.length){t.textContent=e;return}var o;try{o=new RegExp("("+r.map(function(m){return m.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}).join("|")+")","gi")}catch{t.textContent=e;return}for(var a=e.split(o),i=0;i<a.length;i++){if(o.test(a[i])){var d=document.createElement("mark");d.textContent=a[i],t.appendChild(d)}else t.appendChild(document.createTextNode(a[i]));o.lastIndex=0}}function q(t){if(t.key==="Escape"){t.preventDefault(),v();return}if(c){var e=c.querySelectorAll(".site-search-result");if(e.length){for(var n=c.querySelector(".site-search-result.active"),r=-1,o=0;o<e.length;o++)if(e[o]===n){r=o;break}if(t.key==="ArrowDown")t.preventDefault(),A(e,r<e.length-1?r+1:0);else if(t.key==="ArrowUp")t.preventDefault(),A(e,r>0?r-1:e.length-1);else if(t.key==="Enter"&&n){t.preventDefault();var a=n.getAttribute("href");v(),window.location.href=a}}}}function A(t,e){for(var n=0;n<t.length;n++)t[n].classList.toggle("active",n===e);t[e]&&t[e].scrollIntoView({block:"nearest"})}g&&document.addEventListener("keydown",function(t){if((t.metaKey||t.ctrlKey)&&t.key==="k"){var e=document.activeElement&&document.activeElement.tagName;if((e==="INPUT"||e==="TEXTAREA"||e==="SELECT")&&!f)return;t.preventDefault(),f?v():b()}});function O(t,e,n,r){var o=e==="left-center"||e==="bottom-left"||e==="top-left",a=e==="bottom-left"||e==="bottom-right",i=e==="top-left"||e==="top-right";Object.assign(t.style,{right:o?"auto":n+"px",left:o?n+"px":"auto",top:i?r+"px":a?"auto":"50vh",bottom:a?r+"px":"auto",transform:i||a?"none":"translateY(-50%)"})}function H(){if(!document.querySelector(".site-search-floating-trigger")){var t=l.position||"bottom-right",e=l.offsetX!=null?l.offsetX:32,n=l.offsetY!=null?l.offsetY:32,r=document.createElement("button");r.className="site-search-floating-trigger"+(y?"":" site-search-floating-trigger--pill"),r.setAttribute("type","button"),r.setAttribute("aria-label","Search");var o=document.createElement("span");if(o.setAttribute("data-icon","search"),r.appendChild(o),!y){var a=document.createElement("span");a.className="site-search-floating-label",a.textContent="Search",r.appendChild(a)}O(r,t,e,n),document.body.appendChild(r),r.addEventListener("click",b),window.Domma&&Domma.icons&&Domma.icons.scan&&Domma.icons.scan(r)}}function S(){D==="floating"?H():L()}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",S):S()})();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[]
|