/**
* フロントエンド共通: API・設定読み書き・ユーティリティ
*/
const DEFAULT_EXTENSIONS = ['txt', 'md', 'json', 'html', 'css', 'js', 'mjs', 'ts', 'rs'];
export const SETTINGS_KEY = 'telosdb_settings';
export const DEFAULTS = {
min_score: 0.3,
limit: 5,
run_on_login: false,
standard_folders_enabled: false,
monitor_paths: [],
watch_extensions: DEFAULT_EXTENSIONS,
};
export function getApiBase() {
return (typeof window !== 'undefined' && window.API_BASE) ? window.API_BASE : 'http://127.0.0.1:3001';
}
export function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
/**
* Markdown を安全な HTML に変換する(見出し・太字・イタリック・コード・リンク・リスト・ブロック引用など)。
* 出力は許可タグのみで XSS 対策済み。
*/
export function markdownToHtml(md) {
if (md == null || typeof md !== 'string') return '';
const esc = (t) => {
const d = document.createElement('div');
d.textContent = t;
return d.innerHTML;
};
const linkRe = /\[([^\]]*)\]\(([^)]*)\)/g;
const boldRe = /\*\*([^*]+)\*\*|__([^_]+)__/g;
const italicRe = /\*([^*]+)\*|_([^_]+)_/g;
const codeRe = new RegExp('`([^`]+)`', 'g');
const processInline = (line) => {
const placeholders = [];
const ph = (html) => {
const id = placeholders.length;
placeholders.push(html);
return '\uFFFC' + id + '\uFFFC';
};
let s = line
.replace(linkRe, (_, text, href) => ph(`<a href="${esc(href)}" rel="noopener">${esc(text)}</a>`))
.replace(boldRe, (_, a, b) => ph('<strong>' + esc(a != null ? a : b) + '</strong>'))
.replace(italicRe, (_, a, b) => ph('<em>' + esc(a != null ? a : b) + '</em>'))
.replace(codeRe, (_, c) => ph('<code>' + esc(c) + '</code>'));
s = esc(s);
placeholders.forEach((html, i) => {
s = s.replace('\uFFFC' + i + '\uFFFC', html);
});
return s;
};
const lines = md.split(/\r?\n/);
const out = [];
let i = 0;
let inFence = false;
let fenceChar = '';
let codeBlock = [];
const fenceRe = new RegExp('^(`{3,}|~{3,})(.*)$');
const parseTableRow = (str) => {
return str.split('|').map((c) => c.trim()).filter((c) => c !== '');
};
const isTableSeparator = (str) => /^\|[\s\-:]+\|/.test(str) && /^[\s|\-:]+$/.test(str.trim());
while (i < lines.length) {
const line = lines[i];
const fence = line.match(fenceRe);
if (fence) {
if (!inFence) {
inFence = true;
fenceChar = fence[1];
codeBlock = [fence[2] ? esc(fence[2].trim()) : ''];
} else if (line.startsWith(fenceChar)) {
inFence = false;
out.push('<pre><code>' + (codeBlock.length > 1 ? codeBlock.slice(1).join('\n') : codeBlock[0]).replace(/</g, '<').replace(/>/g, '>') + '</code></pre>');
codeBlock = [];
} else {
codeBlock.push(line);
}
i++;
continue;
}
if (inFence) {
codeBlock.push(line);
i++;
continue;
}
const head = line.match(/^(#{1,6})\s+(.*)$/);
if (head) {
const level = head[1].length;
out.push(`<h${level}>${processInline(head[2])}</h${level}>`);
i++;
continue;
}
if (line.includes('|') && line.trim().length > 0) {
const tableRows = [];
let j = i;
while (j < lines.length && lines[j].includes('|')) {
tableRows.push(lines[j]);
j++;
}
if (tableRows.length >= 2) {
const headerCells = parseTableRow(tableRows[0]);
const isSep = isTableSeparator(tableRows[1]);
const bodyStart = isSep ? 2 : 1;
if (headerCells.length > 0) {
let tableHtml = '<table><thead><tr>';
headerCells.forEach((c) => { tableHtml += '<th>' + processInline(c) + '</th>'; });
tableHtml += '</tr></thead><tbody>';
for (let r = bodyStart; r < tableRows.length; r++) {
const cells = parseTableRow(tableRows[r]);
if (cells.length > 0) {
tableHtml += '<tr>';
cells.forEach((c) => { tableHtml += '<td>' + processInline(c) + '</td>'; });
tableHtml += '</tr>';
}
}
tableHtml += '</tbody></table>';
out.push(tableHtml);
}
i = j;
continue;
}
}
if (line.startsWith('> ')) {
const quote = [];
while (i < lines.length && lines[i].startsWith('> ')) {
quote.push(processInline(lines[i].slice(2)));
i++;
}
out.push('<blockquote>' + quote.join('<br>') + '</blockquote>');
continue;
}
if (line.match(/^[-*]\s/)) {
const items = [];
while (i < lines.length && lines[i].match(/^[-*]\s/)) {
items.push('<li>' + processInline(lines[i].replace(/^[-*]\s+/, '')) + '</li>');
i++;
}
out.push('<ul>' + items.join('') + '</ul>');
continue;
}
if (line.match(/^\d+\.\s/)) {
const items = [];
while (i < lines.length && lines[i].match(/^\d+\.\s/)) {
items.push('<li>' + processInline(lines[i].replace(/^\d+\.\s+/, '')) + '</li>');
i++;
}
out.push('<ol>' + items.join('') + '</ol>');
continue;
}
if (line.trim() === '') {
out.push('<br>');
i++;
continue;
}
out.push('<p>' + processInline(line) + '</p>');
i++;
}
if (inFence && codeBlock.length) {
out.push('<pre><code>' + codeBlock.join('\n').replace(/</g, '<').replace(/>/g, '>') + '</code></pre>');
}
return out.join('');
}
export async function callMcp(method, params = {}) {
const res = await fetch(`${getApiBase()}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
method,
params,
id: Date.now(),
}),
});
const data = await res.json();
if (data.error) throw new Error(data.error.message || JSON.stringify(data.error));
return data.result;
}
export function parseResultText(result) {
const content = result?.content;
if (!content || !Array.isArray(content) || content.length === 0) return null;
if (content[0].type === 'text' && content[0].text) {
try {
return JSON.parse(content[0].text);
} catch (_) {
return content[0].text;
}
}
return null;
}
export async function loadSettingsFromFile() {
try {
const { invoke } = await import('@tauri-apps/api/core');
const fileSettings = await invoke('get_app_settings');
if (fileSettings && typeof fileSettings === 'object') {
return {
min_score: fileSettings.min_score ?? DEFAULTS.min_score,
limit: fileSettings.limit ?? DEFAULTS.limit,
run_on_login: Boolean(fileSettings.run_on_login),
standard_folders_enabled: Boolean(fileSettings.standard_folders_enabled),
monitor_paths: Array.isArray(fileSettings.monitor_paths) ? fileSettings.monitor_paths : DEFAULTS.monitor_paths,
watch_extensions: Array.isArray(fileSettings.watch_extensions) ? fileSettings.watch_extensions : DEFAULTS.watch_extensions,
};
}
} catch (_) {
try {
const res = await fetch(`${getApiBase()}/settings`);
if (!res.ok) return null;
const fileSettings = await res.json();
if (fileSettings && typeof fileSettings === 'object') {
return {
min_score: fileSettings.min_score ?? DEFAULTS.min_score,
limit: fileSettings.limit ?? DEFAULTS.limit,
run_on_login: Boolean(fileSettings.run_on_login),
standard_folders_enabled: Boolean(fileSettings.standard_folders_enabled),
monitor_paths: Array.isArray(fileSettings.monitor_paths) ? fileSettings.monitor_paths : DEFAULTS.monitor_paths,
watch_extensions: Array.isArray(fileSettings.watch_extensions) ? fileSettings.watch_extensions : DEFAULTS.watch_extensions,
};
}
} catch (_) {}
}
return null;
}
export async function saveSettingsToFile(payload) {
const toPersist = {
min_score: payload.min_score,
limit: payload.limit,
run_on_login: payload.run_on_login,
standard_folders_enabled: payload.standard_folders_enabled,
monitor_paths: payload.monitor_paths,
watch_extensions: payload.watch_extensions,
};
const hasRemovals = Array.isArray(payload.remove_from_index_paths) && payload.remove_from_index_paths.length > 0;
if (hasRemovals) {
try {
const res = await fetch(`${getApiBase()}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...toPersist, remove_from_index_paths: payload.remove_from_index_paths }),
});
return res.ok;
} catch (_) {
return false;
}
}
try {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('set_app_settings', { settings: toPersist });
return true;
} catch (_) {
try {
const res = await fetch(`${getApiBase()}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(toPersist),
});
return res.ok;
} catch (_) {
return false;
}
}
}