@tiagojct/subsub 0.17.0 → 0.19.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/web/app.js CHANGED
@@ -83,11 +83,21 @@ marked.use({
83
83
  if (!SAFE_LINK.test(token.href ?? "")) return text;
84
84
  return `<a href="${escapeHtml(token.href)}" target="_blank" rel="noopener noreferrer">${text}</a>`;
85
85
  },
86
+ code(token) {
87
+ return (token.lang === "mermaid" && flowHtml(token.text)) || false;
88
+ },
86
89
  codespan(token) {
87
90
  const code = `<code>${escapeHtml(token.text)}</code>`;
88
91
  return NOTE_PATH.test(token.text) ? noteLink(token.text, code) : code;
89
92
  },
90
93
  image(token) {
94
+ // In the notes pane, an image next to the note (a figure cut out of the PDF) is shown;
95
+ // anywhere else, and for any other address, only its description.
96
+ const href = decodeURI(token.href ?? "");
97
+ if (noteDir && /^(?![a-z][\w+.-]*:|[\\/])[^?#]+\.(png|jpe?g|gif|webp)$/i.test(href)) {
98
+ const src = `/api/note-file?path=${encodeURIComponent(`${noteDir}/${href}`)}`;
99
+ return `<img class="note-img" src="${escapeHtml(src)}" alt="${escapeHtml(token.text ?? "")}" loading="lazy">`;
100
+ }
91
101
  return escapeHtml(token.text || token.href || "");
92
102
  },
93
103
  table(token) {
@@ -110,6 +120,45 @@ marked.use({
110
120
  },
111
121
  },
112
122
  });
123
+ /**
124
+ * The flow of works that /lit writes as Mermaid (a chain of boxes, with dotted branches to the side
125
+ * for what was left out), drawn as boxes. Any other diagram stays as code; Obsidian draws it.
126
+ */
127
+ function flowHtml(src) {
128
+ const lines = String(src).split("\n").map((l) => l.trim()).filter(Boolean);
129
+ if (!/^(flowchart|graph)\s+(TD|TB)\b/.test(lines.shift() ?? "")) return null;
130
+ const label = {};
131
+ const next = {};
132
+ const side = {};
133
+ const into = new Set();
134
+ for (const line of lines) {
135
+ const n = /^(\w+)\["([^"]*)"\]$/.exec(line);
136
+ if (n) {
137
+ label[n[1]] = n[2];
138
+ continue;
139
+ }
140
+ const m = /^(\w+)(?:\["([^"]*)"\])?\s*(-->|-\.->)\s*(\w+)(?:\["([^"]*)"\])?$/.exec(line);
141
+ if (!m) return null;
142
+ const [, a, la, arrow, b, lb] = m;
143
+ if (la !== undefined) label[a] = la;
144
+ if (lb !== undefined) label[b] = lb;
145
+ if (arrow === "-->") {
146
+ if (next[a]) return null; // a branch of the main chain: not this kind of diagram
147
+ next[a] = b;
148
+ into.add(b);
149
+ } else (side[a] ??= []).push(b);
150
+ }
151
+ const rows = [];
152
+ const seen = new Set();
153
+ for (let id = Object.keys(next).find((k) => !into.has(k)); id && !seen.has(id); id = next[id]) {
154
+ seen.add(id);
155
+ const sides = (side[id] ?? []).map((s) => `<div class="flow-side">${escapeHtml(label[s] ?? s)}</div>`).join("");
156
+ rows.push(`<div class="flow-row"><div class="flow-box">${escapeHtml(label[id] ?? id)}</div>${sides}</div>`);
157
+ }
158
+ if (rows.length < 2) return null;
159
+ return `<div class="flow">${rows.join("")}</div>`;
160
+ }
161
+
113
162
  function md(text) {
114
163
  try {
115
164
  return marked.parse(String(text ?? ""));
@@ -344,6 +393,7 @@ function searchLibrary() {
344
393
  // ---------------------------------------------------------------- notes pane
345
394
 
346
395
  let noteShown = null; // the note on screen, or null for the list
396
+ let noteDir = ""; // while a note is rendered: its folder, for the images next to it
347
397
  let notesList = [];
348
398
 
349
399
  function showPane(open) {
@@ -379,6 +429,7 @@ async function showNoteList() {
379
429
  $("note-name").textContent = t("notes");
380
430
  $("note-back").hidden = true;
381
431
  $("note-app").hidden = true;
432
+ $("note-read").hidden = true;
382
433
  $("note-filter").hidden = false;
383
434
  showPane(true);
384
435
  try {
@@ -406,9 +457,20 @@ async function openNote(path) {
406
457
  $("note-filter").hidden = true;
407
458
  const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(data.text);
408
459
  const content = el("div", { class: "md" });
460
+ noteDir = data.path.replace(/[\\/][^\\/]*$/, "");
409
461
  content.innerHTML = md(front ? data.text.slice(front[0].length) : data.text);
462
+ noteDir = "";
410
463
  attachCopyButtons(content);
411
- $("note-body").replaceChildren(front ? el("details", { class: "front" }, el("summary", { text: t("properties") }), el("pre", { text: front[1] })) : "", content);
464
+ attachTableButtons(content, data.name.replace(/\.md$/i, ""));
465
+ // The evidence line: what the note was written from, drawn solid, dashed or dotted.
466
+ const evidence = front && /^evidence:\s*(.+?)\s*$/m.exec(front[1])?.[1];
467
+ const kind = evidence && (/full/i.test(evidence) ? "ev-full" : /abstract/i.test(evidence) ? "ev-abstract" : /metadata/i.test(evidence) ? "ev-meta" : "");
468
+ $("note-body").replaceChildren(
469
+ evidence ? el("p", { class: `evidence ${kind}` }, "evidence: ", el("span", { text: evidence })) : "",
470
+ front ? el("details", { class: "front" }, el("summary", { text: t("properties") }), el("pre", { text: front[1] })) : "",
471
+ content,
472
+ );
473
+ $("note-read").hidden = false;
412
474
  $("note-body").scrollTop = 0;
413
475
  showPane(true);
414
476
  }
@@ -519,17 +581,40 @@ function exportConversation() {
519
581
  if (!msgs.length) return;
520
582
  const date = new Date().toISOString().slice(0, 10);
521
583
  const content = `# ${t("exportHeading")} (${date})\n\n${t("exportMode")}: ${t(mode())}\n\n---\n\n${msgs.join("\n---\n\n")}`;
522
- const blob = new Blob([content], { type: "text/markdown;charset=utf-8" });
523
- const url = URL.createObjectURL(blob);
524
- const a = document.createElement("a");
525
- a.href = url;
526
- a.download = `Sub-Sub-${date}.md`;
584
+ download(content, `Sub-Sub-${date}.md`, "text/markdown;charset=utf-8");
585
+ }
586
+
587
+ /** Save text as a file in the downloads folder. */
588
+ function download(content, name, type) {
589
+ const url = URL.createObjectURL(new Blob([content], { type }));
590
+ const a = el("a", { href: url, download: name });
527
591
  document.body.append(a);
528
592
  a.click();
529
593
  a.remove();
530
594
  URL.revokeObjectURL(url);
531
595
  }
532
596
 
597
+ /** A table as CSV for Excel, Rayyan or Covidence. Excel in Portuguese expects semicolons; the BOM keeps the accents. */
598
+ function tableCsv(table) {
599
+ const sep = lang === "pt" ? ";" : ",";
600
+ const cell = (c) => {
601
+ const v = c.textContent.replace(/\s+/g, " ").trim(); // not innerText: the header is upper case only in CSS
602
+ return /[",;\n]/.test(v) ? `"${v.replaceAll('"', '""')}"` : v;
603
+ };
604
+ return `\ufeff${[...table.rows].map((r) => [...r.cells].map(cell).join(sep)).join("\r\n")}\r\n`;
605
+ }
606
+
607
+ /** A "Download CSV" button under each table (evidence tables, extraction sheets, matrices). */
608
+ function attachTableButtons(root, name) {
609
+ const wraps = [...root.querySelectorAll(".md-table-wrap")];
610
+ wraps.forEach((wrap, i) => {
611
+ if (wrap.nextElementSibling?.classList.contains("table-actions")) return;
612
+ const file = `${name}${wraps.length > 1 ? `-${i + 1}` : ""}.csv`;
613
+ const btn = el("button", { type: "button", class: "msg-action-btn", text: t("downloadCsv"), onclick: () => download(tableCsv(wrap.querySelector("table")), file, "text/csv;charset=utf-8") });
614
+ wrap.after(el("div", { class: "table-actions" }, btn));
615
+ });
616
+ }
617
+
533
618
  function closeSide() {
534
619
  $("side").classList.remove("open");
535
620
  $("menu-btn").setAttribute("aria-expanded", "false");
@@ -801,6 +886,7 @@ function renderAssistantFinal(message, msg = startAssistant()) {
801
886
  const node = el("div", { class: "md" });
802
887
  node.innerHTML = md(c.text);
803
888
  attachCopyButtons(node);
889
+ attachTableButtons(node, `Sub-Sub-${new Date().toISOString().slice(0, 10)}`);
804
890
  msg.box.append(node);
805
891
  } else if (c.type === "toolCall") {
806
892
  const row = toolRow(c.id, c.name, c.arguments);
@@ -1436,6 +1522,14 @@ $("library-search").addEventListener("input", searchLibrary);
1436
1522
  $("notes-btn").addEventListener("click", () => ($("note-pane").hidden ? showNoteList() : showPane(false)));
1437
1523
  $("note-back").addEventListener("click", showNoteList);
1438
1524
  $("note-close").addEventListener("click", () => showPane(false));
1525
+ // Reading mode for notes: larger type and wider spacing, remembered on this computer.
1526
+ function setReading(on) {
1527
+ $("note-pane").classList.toggle("reading", on);
1528
+ $("note-read").setAttribute("aria-pressed", String(on));
1529
+ localStorage.setItem("subsub-reading", String(on));
1530
+ }
1531
+ $("note-read").addEventListener("click", () => setReading(!$("note-pane").classList.contains("reading")));
1532
+ setReading(localStorage.getItem("subsub-reading") === "true");
1439
1533
  $("note-app").addEventListener("click", () => noteShown && api("/api/open", { path: noteShown.path }).catch((err) => note(err.message, "warning")));
1440
1534
  $("note-filter").addEventListener("input", renderNoteList);
1441
1535
  $("note-pane").addEventListener("keydown", (e) => {
Binary file
Binary file
package/web/i18n.js CHANGED
@@ -109,6 +109,9 @@ export const STRINGS = {
109
109
  notesHint: "The notes in your Sub-Sub folder",
110
110
  allNotes: "All notes",
111
111
  properties: "Properties",
112
+ readingMode: "Reading mode",
113
+ readingModeHint: "Larger type and wider line spacing for the note",
114
+ downloadCsv: "Download CSV",
112
115
  filterNotes: "Filter notes",
113
116
  noNotes: "No notes yet.",
114
117
  openInApp: "Open in app",
@@ -248,6 +251,9 @@ export const STRINGS = {
248
251
  notesHint: "As notas da sua pasta do Sub-Sub",
249
252
  allNotes: "Todas as notas",
250
253
  properties: "Propriedades",
254
+ readingMode: "Modo de leitura",
255
+ readingModeHint: "Letra maior e mais espaço entre linhas na nota",
256
+ downloadCsv: "Transferir CSV",
251
257
  filterNotes: "Filtrar notas",
252
258
  noNotes: "Ainda não há notas.",
253
259
  openInApp: "Abrir na aplicação",
@@ -293,7 +299,7 @@ export const ACTIONS = {
293
299
  { cmd: "lit", en: "Literature review", pt: "Revisão da literatura" },
294
300
  { cmd: "lit reading list:", en: "Reading list", pt: "Lista de leitura" },
295
301
  { cmd: "compare", en: "Compare sources", pt: "Comparar fontes" },
296
- { cmd: "lit-note", item: true, en: "Literature note", pt: "Nota de leitura" },
302
+ { cmd: "lit-note", item: true, en: "Literature note", pt: "Nota de literatura" },
297
303
  { cmd: "synthesis", en: "Synthesis", pt: "Síntese" },
298
304
  { cmd: "gaps", en: "What is missing", pt: "O que falta" },
299
305
  { cmd: "manuscript", en: "Check a manuscript", pt: "Verificar um manuscrito" },
@@ -311,6 +317,7 @@ export const ACTIONS = {
311
317
  { cmd: "import-queue", send: true, en: "Import the queue", pt: "Importar a fila" },
312
318
  { cmd: "history", send: true, en: "Recent changes", pt: "Alterações recentes" },
313
319
  { cmd: "undo", send: true, en: "Undo the last change", pt: "Desfazer a última alteração" },
320
+ { cmd: "feedback", en: "Report a problem", pt: "Comunicar um problema" },
314
321
  ],
315
322
  };
316
323
 
@@ -332,6 +339,7 @@ export const SUGGESTIONS = {
332
339
  export const TOOLS = {
333
340
  en: {
334
341
  find_items: "search the library",
342
+ get_figure: "cut a figure out of the PDF",
335
343
  standalone_items: "list files without a parent item",
336
344
  set_parent_items: "give files a parent item",
337
345
  find_reference: "look up a reference",
@@ -392,6 +400,7 @@ export const TOOLS = {
392
400
  },
393
401
  pt: {
394
402
  find_items: "pesquisar na biblioteca",
403
+ get_figure: "recortar uma figura do PDF",
395
404
  standalone_items: "listar ficheiros sem item principal",
396
405
  set_parent_items: "dar um item principal aos ficheiros",
397
406
  find_reference: "procurar uma referência",
package/web/icon.svg CHANGED
@@ -1,6 +1,6 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
- <rect width="32" height="32" rx="7" fill="#1d4f7c"/>
3
- <rect x="5.5" y="8" width="21" height="16" rx="1.5" fill="#f7f6f2"/>
4
- <path d="M9 12.5h8M9 16h14" stroke="#1d4f7c" stroke-width="1.6" stroke-linecap="round"/>
5
- <circle cx="16" cy="20.3" r="1.5" fill="#1d4f7c"/>
2
+ <rect width="32" height="32" rx="7" fill="#2c5d87"/>
3
+ <rect x="5.5" y="8" width="21" height="16" rx="1.5" fill="#f4f5f1"/>
4
+ <path d="M9 12.5h8M9 16h14" stroke="#2c5d87" stroke-width="1.6" stroke-linecap="round"/>
5
+ <circle cx="16" cy="20.3" r="1.5" fill="#2c5d87"/>
6
6
  </svg>
package/web/index.html CHANGED
@@ -6,7 +6,7 @@
6
6
  <meta name="color-scheme" content="light dark">
7
7
  <title>Sub-Sub</title>
8
8
  <link rel="icon" href="/icon.svg" type="image/svg+xml">
9
- <link rel="preload" href="/fonts/IBMPlexSans.woff2" as="font" type="font/woff2" crossorigin>
9
+ <link rel="preload" href="/fonts/AtkinsonHyperlegibleNext.woff2" as="font" type="font/woff2" crossorigin>
10
10
  <link rel="stylesheet" href="/app.css">
11
11
  <script type="module" src="/app.js"></script>
12
12
  </head>
@@ -86,6 +86,7 @@
86
86
  <div class="note-head">
87
87
  <button type="button" class="link-btn" id="note-back" data-i18n="allNotes">All notes</button>
88
88
  <span class="note-name" id="note-name"></span>
89
+ <button type="button" class="link-btn" id="note-read" aria-pressed="false" data-i18n="readingMode" data-i18n-title="readingModeHint" hidden>Reading mode</button>
89
90
  <button type="button" class="link-btn" id="note-app" data-i18n="openInApp" hidden>Open in app</button>
90
91
  <button type="button" class="link-btn" id="note-close" data-i18n="close">Close</button>
91
92
  </div>
Binary file
Binary file
Binary file