Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions analyzer-views/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ sequence.sequence = analyzerSeqText; // the text of spec/analyzer.seq
sequence.addEventListener("nlp-open", (e) => openFile(e.detail.path));

const kb = document.createElement("nlp-knowledge-base");
kb.files = [{ path: "kb/user/colors.dict", bytes: 3000 }]; // sizes are optional
kb.files = [{ path: "kb/user/colors.dict", bytes: 3000, // sizes are optional,
description: kbDescription(colorsDictText) }]; // and so is the mouse-over
kb.addEventListener("nlp-open", (e) => openFile(e.detail.path));

sidebar.append(sequence, kb);
Expand All @@ -42,7 +43,7 @@ Every list element takes:

| | |
|---|---|
| `files` (property) | The paths to list: strings, or `{ path, bytes }` objects to show sizes. `<nlp-trees>` takes `trees` instead. |
| `files` (property) | The paths to list: strings, or `{ path, bytes }` objects to show sizes. On `<nlp-knowledge-base>`, a `description` too: the file's mouse-over, above its path. `<nlp-trees>` takes `trees` instead. |
| `selected` (property) | The path that's open now, drawn as selected. |
| `changed` (property) | Paths with unsaved edits. Each gets a mark, plus a revert button when the element has `revertable`. |
| `heading` (attribute) | The list's heading. `heading=""` shows no heading. |
Expand Down Expand Up @@ -144,7 +145,11 @@ follow the same light and dark choice.

- `passes(seqText, files)` reads `analyzer.seq` into passes, numbered the way the engine
numbers them. A switched-off pass keeps its number, and a folder or stub gets none.
- `shownInKnowledgeBase(path)` says whether a file is listed in the knowledge base.
- `shownInKnowledgeBase(path)` says whether a file is listed in the knowledge base, and
`kbLabel(path)` names it there: its path under `kb/user/`.
- `kbDescription(text)` is what a `.dict` or `.kbb` file holds, from the comment
paragraph at its top, as the extension's knowledge base view shows on mouse-over. Only
the first 4 KB are read, so a head of the file is enough.
- `passTooltip`, `passLabel`, `passIcon`, `fileIcon` and `fileSize` give each row's
mouse-over, label, icon and size.
- `outputOrder`, `treeOrder`, `treeLabel`, `treeTitle` and `treeNote` order and describe
Expand Down
4 changes: 2 additions & 2 deletions analyzer-views/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion analyzer-views/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@visualtext/analyzer-views",
"private": true,
"version": "0.5.0",
"version": "0.6.0",
"description": "An NLP++ analyzer's sequence, knowledge base, run results, values, code and log, shown the way the NLP++ extension for VS Code shows them, for any web page.",
"type": "module",
"repository": {
Expand Down
11 changes: 7 additions & 4 deletions analyzer-views/src/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
// seq.sequence = seqText;
// seq.addEventListener("nlp-open", (e) => open(e.detail.path));
//
// Properties: files paths, or { path, bytes } to show sizes (all but <nlp-trees>)
// Properties: files paths, or { path, bytes } to show sizes (all but <nlp-trees>);
// on <nlp-knowledge-base>, { description } too, the mouse-over
// (kbDescription of the file's text)
// sequence the text of spec/analyzer.seq (<nlp-sequence>)
// trees { name, pass, passName, bytes } and skipped, names too large
// to keep (<nlp-trees>); on <nlp-sequence>, the trees a -DEV run
Expand All @@ -39,7 +41,7 @@
// LIGHT DOM, not a shadow root: the rows are ordinary buttons a page can style, test and
// query (button[data-path]). style.css draws them; its --nlp-* properties theme them.

import { type AnalyzerFile, fileSize, shownInKnowledgeBase, toFile } from "./files.js";
import { type AnalyzerFile, fileSize, kbLabel, shownInKnowledgeBase, toFile } from "./files.js";
import { nlpTokens } from "./highlight.js";
import { type IconName, fileIcon, iconElement, passIcon } from "./icons.js";
import { type NlpLanguage, NLP_LANGUAGES, langFor } from "./languages.js";
Expand Down Expand Up @@ -310,10 +312,11 @@ export class NlpKnowledgeBase extends FileList {

protected rows(): Row[] {
return this.files.filter((f) => shownInKnowledgeBase(f.path)).map((f) => ({
label: f.path.slice("kb/".length),
label: kbLabel(f.path),
path: f.path,
icon: fileIcon(f.path),
title: f.path,
// What the file holds, over where it lives, as the extension's mouse-over.
title: f.description ? `${f.description}\n\n${f.path}` : f.path,
size: fileSize(f.bytes),
}));
}
Expand Down
55 changes: 54 additions & 1 deletion analyzer-views/src/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
// PURE: no DOM.

// A file of the analyzer: its path inside the analyzer (spec/..., kb/..., input/...),
// and its size when the host knows it.
// its size when the host knows it, and, for a knowledge-base file, what it holds
// (kbDescription of its text) to show when the mouse is over it.
export interface AnalyzerFile {
path: string;
bytes?: number;
description?: string;
}

export function toFile(file: string | AnalyzerFile): AnalyzerFile {
Expand All @@ -20,6 +22,57 @@ export function shownInKnowledgeBase(path: string): boolean {
return path.startsWith("kb/") && /\.(dict|kbb)$/i.test(path);
}

// A knowledge-base file's name in the list: its path under kb/user/, where an analyzer's
// own files live -- "colors.dict" rather than "kb/user/colors.dict".
export function kbLabel(path: string): string {
return path.replace(/^kb\/(user\/)?/, "");
}

// What a .dict or .kbb file holds, as its author says in the comment at the top of it: the
// opening paragraph of '#' or /* */ comment lines, as the extension's KNOWLEDGE BASE view
// shows it on mouse-over (kbView.ts fileDescription). A bare '#' ends the paragraph; a '/*'
// alone on the line opening a block comment is stepped over. "" when there is none.
//
// Only the head is read: en-full.kbb is 10 MB, and the description is at the top.
const DESCRIPTION_HEAD = 4096;
const DESCRIPTION_LINES = 8;

export function kbDescription(text: string | null | undefined): string {
const all = String(text ?? "");
const lines = all.slice(0, DESCRIPTION_HEAD).split(/\r?\n/);
if (all.length > DESCRIPTION_HEAD) lines.pop(); // cut mid-line: not whole
const out: string[] = [];
let open = false; // inside a /* */ comment begun on an earlier line
for (const line of lines) {
if (out.length >= DESCRIPTION_LINES) break;
const trimmed = line.trim();
if (open || trimmed.startsWith("/*")) {
// A block-comment line counts only when nothing follows its close.
const close = trimmed.indexOf("*/", open ? 0 : 2);
if (close >= 0 && trimmed.slice(close + 2).trim()) break;
open = close < 0;
} else if (!trimmed.startsWith("#")) {
break;
}
const words = stripCommentMarkers(trimmed);
if (!words) {
if (!out.length) continue; // '/*' on a line of its own opens the block
break; // a marker-only line closes the paragraph
}
out.push(words);
}
return out.join("\n");
}

// One comment line without its markers: '#', '/*', '*/', and the '*' block comments'
// continuation lines are often drawn with.
function stripCommentMarkers(line: string): string {
let text = line;
if (text.startsWith("/*")) text = text.slice(2);
if (text.endsWith("*/")) text = text.slice(0, -2);
return text.replace(/^[#*\s]+/, "").trim();
}

// A size for a list: "3 KB", "1.2 MB". Unknown or empty prints nothing rather than
// claiming "0 KB".
export function fileSize(bytes: number | null | undefined): string {
Expand Down
9 changes: 8 additions & 1 deletion analyzer-views/test/elements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,16 @@ describe("<nlp-knowledge-base>", () => {
document.body.append(el);
expect(el.querySelector("h3")?.textContent).toBe("Knowledge base");
expect(paths(el)).toEqual(["kb/user/colors.dict", "kb/user/facts.kbb"]);
expect([...el.querySelectorAll(".nlp-name")].map((n) => n.textContent)).toEqual(["user/colors.dict", "user/facts.kbb"]);
expect([...el.querySelectorAll(".nlp-name")].map((n) => n.textContent)).toEqual(["colors.dict", "facts.kbb"]);
expect([...el.querySelectorAll(".nlp-size")].map((n) => n.textContent)).toEqual(["3 KB"]);
});
it("says on mouse-over what a file holds, over its path", () => {
const el = document.createElement("nlp-knowledge-base") as NlpKnowledgeBase;
el.files = [{ path: "kb/user/colors.dict", description: "Colour words" }, "kb/user/facts.kbb"];
document.body.append(el);
expect([...el.querySelectorAll(".nlp-name")].map((n) => n.getAttribute("title")))
.toEqual(["Colour words\n\nkb/user/colors.dict", "kb/user/facts.kbb"]);
});
it("hides itself when the analyzer has only .kb files", () => {
const el = document.createElement("nlp-knowledge-base");
el.files = ["kb/user/hier.kb", "kb/user/word.kb"];
Expand Down
18 changes: 17 additions & 1 deletion analyzer-views/test/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// The rules with no DOM at all, as a server or another page would use them.
import { describe, expect, it } from "vitest";
import {
fileIcon, fileSize, outputOrder, passComment, passIcon, passLabel, passTooltip, passes, shownInKnowledgeBase,
fileIcon, fileSize, outputOrder, passComment, passIcon, passLabel, passTooltip, passes, shownInKnowledgeBase, kbDescription, kbLabel,
filledFields, langFor, logLines, matchCount, matchesIn, problemWhere, problemsInLog, readOutput, ruleMatches,
treeLabel, treeNote, treeOrder, treeTitle,
} from "../src/rules.js";
Expand Down Expand Up @@ -89,6 +89,22 @@ describe("the knowledge base", () => {
];
expect(files.filter(shownInKnowledgeBase)).toEqual(["kb/user/colors.dict", "kb/user/en-full.DICT", "kb/user/facts.kbb"]);
});
it("names a file by its path under kb/user/", () => {
expect(["kb/user/colors.dict", "kb/general/en.kbb", "kb/facts.kbb"].map(kbLabel)).toEqual(["colors.dict", "general/en.kbb", "facts.kbb"]);
});
it("describes a file by the comment paragraph at its top, as the extension does", () => {
expect(kbDescription("# Colour words\n# for the paint analyzer\n#\n# Maintained by hand\nred\tcolor=red\n"))
.toBe("Colour words\nfor the paint analyzer");
expect(kbDescription("/*\n * Facts about firms\n */\nfirm\n")).toBe("Facts about firms");
expect(kbDescription("/* One line */\r\nfirm\r\n")).toBe("One line");
expect(kbDescription("/* then */ red\n")).toBe("");
expect(kbDescription("red\tcolor=red\n# not at the top\n")).toBe("");
expect(kbDescription(undefined)).toBe("");
});
it("reads only the head of a large file", () => {
expect(kbDescription(`# Every English word\n${"word\n".repeat(1_000_000)}`)).toBe("Every English word");
expect(kbDescription(Array.from({ length: 20 }, (_, i) => `# line ${i}`).join("\n")).split("\n")).toHaveLength(8);
});
it("prints a size, and nothing for one it does not know", () => {
expect([fileSize(3000), fileSize(100), fileSize(2.5 * 1024 * 1024), fileSize(0), fileSize(undefined)])
.toEqual(["3 KB", "1 KB", "2.5 MB", "", ""]);
Expand Down
9 changes: 7 additions & 2 deletions studio/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { languageFor } from "./lsp/convert";
import { type AnalyzerEntry, fileUri, loadFiles, loadIndex, pathOf } from "./analyzers";
import {
type IconName, type NlpKnowledgeBase, type NlpSequence, type Pass, type TreeFile as ListedTree,
fileSize, iconElement, passes, ruleMatches, treeTitle,
fileSize, iconElement, kbDescription, passes, ruleMatches, shownInKnowledgeBase, treeTitle,
} from "@visualtext/analyzer-views";
import {
type Account, type CommitResult, type FoundAnalyzer, type RecentAnalyzer, type RepoAnalyzers, type Repository,
Expand Down Expand Up @@ -887,7 +887,12 @@ export class Studio {
sequence.addEventListener("nlp-open-matches", (e) => void this.openMatches(e.detail.path));
listen(sequence);
const kb = document.createElement("nlp-knowledge-base");
kb.files = entry.files;
// Each file's opening comment is its mouse-over, as in the extension. Only its top
// lines are taken: a .kbb can run to megabytes.
const head = (model: monaco.editor.ITextModel | undefined) =>
model?.getValueInRange(new monaco.Range(1, 1, Math.min(model.getLineCount(), 20) + 1, 1));
kb.files = entry.files.filter(shownInKnowledgeBase)
.map((path) => ({ path, description: kbDescription(head(this.models.get(path))) }));
listen(kb);

// The studio's own lists, drawn with the same rows until they move there too.
Expand Down
Loading