lognal 0.1.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/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/core/filter.d.ts +29 -0
- package/dist/core/filter.js +82 -0
- package/dist/core/layout/entry-lines.d.ts +16 -0
- package/dist/core/layout/entry-lines.js +120 -0
- package/dist/core/layout/layout.d.ts +152 -0
- package/dist/core/layout/layout.js +784 -0
- package/dist/core/layout/row-index.d.ts +33 -0
- package/dist/core/layout/row-index.js +99 -0
- package/dist/core/layout/types.d.ts +128 -0
- package/dist/core/layout/types.js +8 -0
- package/dist/core/store.d.ts +96 -0
- package/dist/core/store.js +204 -0
- package/dist/core/text/ansi.d.ts +20 -0
- package/dist/core/text/ansi.js +187 -0
- package/dist/core/text/graphemes.d.ts +17 -0
- package/dist/core/text/graphemes.js +70 -0
- package/dist/core/text/line-splitter.d.ts +18 -0
- package/dist/core/text/line-splitter.js +56 -0
- package/dist/core/text/measure.d.ts +10 -0
- package/dist/core/text/measure.js +38 -0
- package/dist/core/text/shape.d.ts +24 -0
- package/dist/core/text/shape.js +220 -0
- package/dist/core/text/unicode-width-data.d.ts +54 -0
- package/dist/core/text/unicode-width-data.js +227 -0
- package/dist/core/text/width.d.ts +20 -0
- package/dist/core/text/width.js +157 -0
- package/dist/core/text/wrap.d.ts +14 -0
- package/dist/core/text/wrap.js +94 -0
- package/dist/core/time.d.ts +10 -0
- package/dist/core/time.js +24 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +9 -0
- package/dist/core/value/preview.d.ts +16 -0
- package/dist/core/value/preview.js +207 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/lognal.css +589 -0
- package/dist/react/LogViewer.d.ts +32 -0
- package/dist/react/LogViewer.js +183 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +2 -0
- package/dist/renderer/canvas/canvas-renderer.d.ts +45 -0
- package/dist/renderer/canvas/canvas-renderer.js +464 -0
- package/dist/renderer/canvas/palette.d.ts +6 -0
- package/dist/renderer/canvas/palette.js +25 -0
- package/dist/renderer/theme.d.ts +3 -0
- package/dist/renderer/theme.js +52 -0
- package/dist/renderer/types.d.ts +82 -0
- package/dist/renderer/types.js +1 -0
- package/dist/sources/console/format.d.ts +32 -0
- package/dist/sources/console/format.js +189 -0
- package/dist/sources/console/hook.d.ts +27 -0
- package/dist/sources/console/hook.js +94 -0
- package/dist/sources/console/recorder.d.ts +41 -0
- package/dist/sources/console/recorder.js +239 -0
- package/dist/sources/console/snapshot.d.ts +22 -0
- package/dist/sources/console/snapshot.js +547 -0
- package/dist/sources/console/table.d.ts +9 -0
- package/dist/sources/console/table.js +87 -0
- package/dist/sources/text/encoding.d.ts +11 -0
- package/dist/sources/text/encoding.js +59 -0
- package/dist/sources/text/follow-file.d.ts +49 -0
- package/dist/sources/text/follow-file.js +109 -0
- package/dist/sources/text/read-file.d.ts +63 -0
- package/dist/sources/text/read-file.js +82 -0
- package/dist/viewer/icons.d.ts +14 -0
- package/dist/viewer/icons.js +30 -0
- package/dist/viewer/input-line.d.ts +46 -0
- package/dist/viewer/input-line.js +144 -0
- package/dist/viewer/labels.d.ts +35 -0
- package/dist/viewer/labels.js +61 -0
- package/dist/viewer/scrollbar.d.ts +27 -0
- package/dist/viewer/scrollbar.js +143 -0
- package/dist/viewer/theme.d.ts +15 -0
- package/dist/viewer/theme.js +105 -0
- package/dist/viewer/viewer.d.ts +217 -0
- package/dist/viewer/viewer.js +1201 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CDGet (https://cdget.com).
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/jooy2/lognal/refs/heads/main/.github/resources/lognal-logo.webp" width="96" height="96" alt="lognal logo" />
|
|
2
|
+
|
|
3
|
+
# lognal
|
|
4
|
+
|
|
5
|
+
[](https://github.com/jooy2/lognal/blob/main/LICENSE) [](https://github.com/jooy2/lognal/actions/workflows/run-test.yml)  [](https://github.com/jooy2) 
|
|
6
|
+
|
|
7
|
+
**lognal** is a log viewer for web pages that looks and behaves like a terminal. It draws log output on a canvas instead of creating a DOM element for every line, so a fast stream of messages and a long history do not slow the page down.
|
|
8
|
+
|
|
9
|
+
> **lognal is not published to npm yet.** The API described here works in this repository and may still change before the first release.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- **Shows the browser console inside your page.** Hook `console.log`, `console.warn`, `console.table`, `console.group` and the rest. Arguments are captured at the moment of the call, and the original console keeps working.
|
|
14
|
+
- **Reads log files.** Open a text file and read it line by line. The encoding is detected, including legacy encodings such as EUC-KR. In Chromium-based browsers, a file picked with the File System Access API can be followed as it grows.
|
|
15
|
+
- **Displays values by type.** Objects, arrays, maps, sets, errors and DOM elements expand and collapse, and `console.table` draws a table.
|
|
16
|
+
- **Accepts commands.** Connect a handler, and the viewer shows an input line and prints the replies.
|
|
17
|
+
- **Handles Korean and other CJK text.** Wide characters stay on the grid, Korean text wraps at spaces, and Enter waits for IME composition to finish.
|
|
18
|
+
|
|
19
|
+
The viewer has a toolbar (follow new logs, clear, scroll to top and bottom, line wrapping, text filter and level filter), a status bar, timestamps, light and dark themes, and a custom scrollbar. Every part can be turned off or restyled with CSS custom properties.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install lognal
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
import { LogViewer } from 'lognal';
|
|
29
|
+
import 'lognal/style.css';
|
|
30
|
+
|
|
31
|
+
// The container needs a height.
|
|
32
|
+
const viewer = new LogViewer(document.getElementById('logs'), {
|
|
33
|
+
timestamps: true,
|
|
34
|
+
core: { maxEntries: 20000 }
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
viewer.hookConsole();
|
|
38
|
+
console.log('Hello %s', 'lognal', { id: 1, tags: ['canvas', 'logs'] });
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
With React:
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import { LogViewer } from 'lognal/react';
|
|
45
|
+
import 'lognal/style.css';
|
|
46
|
+
|
|
47
|
+
export const Logs = () => <LogViewer hookConsole style={{ height: 400 }} />;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Reading a file:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import { readTextFile } from 'lognal';
|
|
54
|
+
|
|
55
|
+
input.addEventListener('change', async () => {
|
|
56
|
+
await readTextFile(input.files[0], viewer.store);
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
The documentation is at **[lognal.cdget.com](https://lognal.cdget.com)**, in English and Korean.
|
|
63
|
+
|
|
64
|
+
## Browser support
|
|
65
|
+
|
|
66
|
+
lognal targets current versions of Chrome, Edge, Firefox and Safari. A few features depend on newer platform APIs:
|
|
67
|
+
|
|
68
|
+
| Feature | Requirement |
|
|
69
|
+
| -------------------------------------- | ------------------------------------------------------------------------------ |
|
|
70
|
+
| Grapheme clusters for emoji and Hangul | `Intl.Segmenter`: Chrome 87, Firefox 125, Safari 14.1. A fallback is built in. |
|
|
71
|
+
| Following a growing file | File System Access API, Chromium-based browsers only |
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install
|
|
77
|
+
npm run dev # playground at http://localhost:5173
|
|
78
|
+
npm test # unit tests in Node.js and browser tests in Playwright
|
|
79
|
+
npm run lint
|
|
80
|
+
npm run build
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the project layout and the workflow.
|
|
84
|
+
|
|
85
|
+
## Contributing
|
|
86
|
+
|
|
87
|
+
Anyone can contribute to the project by reporting new issues or submitting a pull request. For more information, please see [CONTRIBUTING.md](CONTRIBUTING.md). Participation is subject to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
|
88
|
+
|
|
89
|
+
To report a security issue, please follow the process described in [SECURITY.md](SECURITY.md).
|
|
90
|
+
|
|
91
|
+
For anything that does not belong in a public issue, write to CDGet at [cdget.com/contact](https://cdget.com/contact).
|
|
92
|
+
|
|
93
|
+
## Sponsor
|
|
94
|
+
|
|
95
|
+
lognal is free to use and maintained in the open. If it saves you time, you can support the work at [cdget.com/donate](https://cdget.com/donate) or through the Sponsor button on GitHub.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
Please see the [LICENSE](LICENSE) file for more information about project owners, usage rights, and more. The character width tables are generated from the Unicode Character Database, whose license is included in `src/core/text/unicode-width-data.ts`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type LogEntry, type LogLevel } from './types.js';
|
|
2
|
+
/** Which entries a viewer shows. */
|
|
3
|
+
export interface LogFilter {
|
|
4
|
+
/** Text an entry must contain. Empty text matches every entry. */
|
|
5
|
+
text?: string;
|
|
6
|
+
/** Whether `text` is a regular expression. */
|
|
7
|
+
regex?: boolean;
|
|
8
|
+
/** Whether letter case must match. */
|
|
9
|
+
caseSensitive?: boolean;
|
|
10
|
+
/** The least severe level shown. */
|
|
11
|
+
minLevel?: LogLevel;
|
|
12
|
+
/** The levels shown. When set, `minLevel` is ignored. */
|
|
13
|
+
levels?: readonly LogLevel[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Returns the text of an entry as the filter sees it: its text parts, and a one-line preview of
|
|
17
|
+
* every value.
|
|
18
|
+
*/
|
|
19
|
+
export declare const entrySearchText: (entry: LogEntry) => string;
|
|
20
|
+
/** A compiled filter. `matches` is `null` when the filter lets every entry through. */
|
|
21
|
+
export interface CompiledFilter {
|
|
22
|
+
matches: ((entry: LogEntry) => boolean) | null;
|
|
23
|
+
/** Finds matches in a line of text, for highlighting. `null` when there is no text filter. */
|
|
24
|
+
pattern: RegExp | null;
|
|
25
|
+
/** Set when `text` is not a valid regular expression. */
|
|
26
|
+
error: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** Turns a filter into a function that tests entries. */
|
|
29
|
+
export declare const compileFilter: (filter: LogFilter | null | undefined) => CompiledFilter;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { errorTitle, previewValue } from './value/preview.js';
|
|
2
|
+
import { LOG_LEVELS } from './types.js';
|
|
3
|
+
/** The most characters of an entry the text filter looks at. */
|
|
4
|
+
const SEARCH_TEXT_LIMIT = 20000;
|
|
5
|
+
const searchTextCache = new WeakMap();
|
|
6
|
+
const valueText = (node) => {
|
|
7
|
+
if (node.kind === 'error') {
|
|
8
|
+
return `${errorTitle(node)}\n${node.stack ?? ''}`;
|
|
9
|
+
}
|
|
10
|
+
return previewValue(node)
|
|
11
|
+
.map((span) => span.text)
|
|
12
|
+
.join('');
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Returns the text of an entry as the filter sees it: its text parts, and a one-line preview of
|
|
16
|
+
* every value.
|
|
17
|
+
*/
|
|
18
|
+
export const entrySearchText = (entry) => {
|
|
19
|
+
const cached = searchTextCache.get(entry);
|
|
20
|
+
if (cached && cached.version === entry.version) {
|
|
21
|
+
return cached.text;
|
|
22
|
+
}
|
|
23
|
+
let text = '';
|
|
24
|
+
for (const part of entry.parts) {
|
|
25
|
+
text += part.type === 'text' ? part.text : valueText(part.value);
|
|
26
|
+
if (text.length > SEARCH_TEXT_LIMIT) {
|
|
27
|
+
text = text.slice(0, SEARCH_TEXT_LIMIT);
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Compose decomposed Hangul and accented letters, so text copied from a macOS file name
|
|
32
|
+
// matches what the user types.
|
|
33
|
+
text = text.normalize('NFC');
|
|
34
|
+
searchTextCache.set(entry, { version: entry.version, text });
|
|
35
|
+
return text;
|
|
36
|
+
};
|
|
37
|
+
const escapeRegExp = (text) => {
|
|
38
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
};
|
|
40
|
+
/** Turns a filter into a function that tests entries. */
|
|
41
|
+
export const compileFilter = (filter) => {
|
|
42
|
+
if (!filter) {
|
|
43
|
+
return { matches: null, pattern: null, error: null };
|
|
44
|
+
}
|
|
45
|
+
const levels = filter.levels
|
|
46
|
+
? new Set(filter.levels)
|
|
47
|
+
: filter.minLevel
|
|
48
|
+
? new Set(LOG_LEVELS.slice(LOG_LEVELS.indexOf(filter.minLevel)))
|
|
49
|
+
: null;
|
|
50
|
+
let pattern = null;
|
|
51
|
+
let error = null;
|
|
52
|
+
if (filter.text) {
|
|
53
|
+
const flags = filter.caseSensitive ? 'g' : 'gi';
|
|
54
|
+
try {
|
|
55
|
+
const text = filter.text.normalize('NFC');
|
|
56
|
+
pattern = new RegExp(filter.regex ? text : escapeRegExp(text), flags);
|
|
57
|
+
}
|
|
58
|
+
catch (caught) {
|
|
59
|
+
error = caught instanceof Error ? caught.message : String(caught);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!levels && !pattern && !error) {
|
|
63
|
+
return { matches: null, pattern: null, error: null };
|
|
64
|
+
}
|
|
65
|
+
const matches = (entry) => {
|
|
66
|
+
if (entry.kind === 'group' && !pattern && !error) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (levels && entry.kind !== 'input' && entry.kind !== 'system' && !levels.has(entry.level)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (error) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (pattern) {
|
|
76
|
+
pattern.lastIndex = 0;
|
|
77
|
+
return pattern.test(entrySearchText(entry));
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
return { matches, pattern, error };
|
|
82
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LogEntry } from '../types.js';
|
|
2
|
+
import type { LogicalLine } from './types.js';
|
|
3
|
+
/** Cells of indentation for every level of nesting. */
|
|
4
|
+
export declare const INDENT_CELLS = 2;
|
|
5
|
+
/** Decides whether the value at a path is shown expanded. */
|
|
6
|
+
export type ExpansionLookup = (path: string) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Returns whether a value path is expanded when the user has not toggled it: errors logged
|
|
9
|
+
* directly are open, so their stack trace is visible, and everything else is closed.
|
|
10
|
+
*/
|
|
11
|
+
export declare const isExpandedByDefault: (entry: LogEntry, path: string) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Builds the logical lines of an entry: one for every line of its text, followed by the rows
|
|
14
|
+
* of every expanded value in the order the values appear.
|
|
15
|
+
*/
|
|
16
|
+
export declare const buildEntryLines: (entry: LogEntry, isExpanded: ExpansionLookup) => LogicalLine[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { errorTitle, formatKey, isExpandable, previewValue } from '../value/preview.js';
|
|
2
|
+
/** Cells of indentation for every level of nesting. */
|
|
3
|
+
export const INDENT_CELLS = 2;
|
|
4
|
+
/**
|
|
5
|
+
* Returns whether a value path is expanded when the user has not toggled it: errors logged
|
|
6
|
+
* directly are open, so their stack trace is visible, and everything else is closed.
|
|
7
|
+
*/
|
|
8
|
+
export const isExpandedByDefault = (entry, path) => {
|
|
9
|
+
if (path.includes('.')) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const part = entry.parts[Number(path)];
|
|
13
|
+
return part?.type === 'value' && part.value.kind === 'error';
|
|
14
|
+
};
|
|
15
|
+
const errorSpans = (node) => {
|
|
16
|
+
return [{ text: errorTitle(node), token: 'error' }];
|
|
17
|
+
};
|
|
18
|
+
const expanderSpan = (path, expanded) => {
|
|
19
|
+
return { icon: 'expander', expanded, action: { type: 'toggle-value', path } };
|
|
20
|
+
};
|
|
21
|
+
const childLines = (node, path, indent, isExpanded, lines) => {
|
|
22
|
+
// Rows without an expander start two cells in, so their text lines up with the text of
|
|
23
|
+
// the rows that have one.
|
|
24
|
+
const textIndent = indent + INDENT_CELLS;
|
|
25
|
+
if (node.kind === 'error' && node.stack) {
|
|
26
|
+
for (const stackLine of node.stack.split('\n')) {
|
|
27
|
+
lines.push({ indent: textIndent, spans: [{ text: stackLine.trim(), token: 'muted' }] });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
node.children?.forEach((child, index) => {
|
|
31
|
+
childLine(node, child, `${path}.${index}`, indent, isExpanded, lines);
|
|
32
|
+
});
|
|
33
|
+
if ((node.omitted ?? 0) > 0) {
|
|
34
|
+
lines.push({ indent: textIndent, spans: [{ text: `… ${node.omitted} more`, token: 'muted' }] });
|
|
35
|
+
}
|
|
36
|
+
if (node.kind === 'element' && node.value) {
|
|
37
|
+
lines.push({ indent, spans: [{ text: `</${node.value}>`, token: 'tag' }] });
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const childLine = (parent, child, path, indent, isExpanded, lines) => {
|
|
41
|
+
const expandable = isExpandable(child.value);
|
|
42
|
+
const expanded = expandable && isExpanded(path);
|
|
43
|
+
const spans = [];
|
|
44
|
+
if (expandable) {
|
|
45
|
+
spans.push(expanderSpan(path, expanded));
|
|
46
|
+
}
|
|
47
|
+
if (parent.kind === 'map' && child.keyValue) {
|
|
48
|
+
spans.push(...previewValue(child.keyValue, true), { text: ' => ', token: 'default' });
|
|
49
|
+
}
|
|
50
|
+
else if (child.key !== undefined && parent.kind !== 'set' && parent.kind !== 'element') {
|
|
51
|
+
spans.push(formatKey(child), { text: ': ', token: 'default' });
|
|
52
|
+
}
|
|
53
|
+
const action = { type: 'toggle-value', path };
|
|
54
|
+
const valueSpans = child.value.kind === 'error' ? errorSpans(child.value) : previewValue(child.value);
|
|
55
|
+
spans.push(...valueSpans.map((span) => (expandable ? { ...span, action } : span)));
|
|
56
|
+
lines.push({ indent: expandable ? indent : indent + INDENT_CELLS, spans });
|
|
57
|
+
if (expanded) {
|
|
58
|
+
childLines(child.value, path, indent + INDENT_CELLS, isExpanded, lines);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Builds the logical lines of an entry: one for every line of its text, followed by the rows
|
|
63
|
+
* of every expanded value in the order the values appear.
|
|
64
|
+
*/
|
|
65
|
+
export const buildEntryLines = (entry, isExpanded) => {
|
|
66
|
+
const baseIndent = entry.groups.length * INDENT_CELLS;
|
|
67
|
+
const lines = [];
|
|
68
|
+
let current = [];
|
|
69
|
+
let currentWraps = true;
|
|
70
|
+
const expandedParts = [];
|
|
71
|
+
if (entry.kind === 'group') {
|
|
72
|
+
current.push({
|
|
73
|
+
icon: 'expander',
|
|
74
|
+
expanded: !entry.collapsed,
|
|
75
|
+
action: { type: 'toggle-group' }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
entry.parts.forEach((part, index) => {
|
|
79
|
+
if (part.type === 'text') {
|
|
80
|
+
const pieces = part.text.split('\n');
|
|
81
|
+
pieces.forEach((piece, pieceIndex) => {
|
|
82
|
+
if (pieceIndex > 0) {
|
|
83
|
+
lines.push({ indent: baseIndent, spans: current, wrap: currentWraps });
|
|
84
|
+
current = [];
|
|
85
|
+
currentWraps = true;
|
|
86
|
+
}
|
|
87
|
+
if (part.wrap === false) {
|
|
88
|
+
currentWraps = false;
|
|
89
|
+
}
|
|
90
|
+
if (piece) {
|
|
91
|
+
current.push({
|
|
92
|
+
text: piece,
|
|
93
|
+
token: part.token,
|
|
94
|
+
style: part.style,
|
|
95
|
+
...(entry.kind === 'group' ? { action: { type: 'toggle-group' } } : {})
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const node = part.value;
|
|
102
|
+
const path = String(index);
|
|
103
|
+
if (!isExpandable(node)) {
|
|
104
|
+
current.push(...previewValue(node));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const expanded = isExpanded(path);
|
|
108
|
+
const action = { type: 'toggle-value', path };
|
|
109
|
+
const title = node.kind === 'error' ? errorSpans(node) : previewValue(node);
|
|
110
|
+
current.push(expanderSpan(path, expanded), ...title.map((span) => ({ ...span, action })));
|
|
111
|
+
if (expanded) {
|
|
112
|
+
expandedParts.push({ node, path });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
lines.push({ indent: baseIndent, spans: current, wrap: currentWraps });
|
|
116
|
+
for (const { node, path } of expandedParts) {
|
|
117
|
+
childLines(node, path, baseIndent + INDENT_CELLS, isExpanded, lines);
|
|
118
|
+
}
|
|
119
|
+
return lines;
|
|
120
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { type CompiledFilter, type LogFilter } from '../filter.js';
|
|
2
|
+
import type { LogStore } from '../store.js';
|
|
3
|
+
import { type ShapeOptions } from '../text/shape.js';
|
|
4
|
+
import type { LogEntry } from '../types.js';
|
|
5
|
+
import type { LineAction, TextPosition, VisualRow, WrapMode } from './types.js';
|
|
6
|
+
export interface LayoutOptions extends ShapeOptions {
|
|
7
|
+
/** How lines longer than the viewer are handled. */
|
|
8
|
+
wrap: WrapMode;
|
|
9
|
+
}
|
|
10
|
+
export declare const DEFAULT_LAYOUT_OPTIONS: LayoutOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Turns the entries of a store into rows for a viewer of a given width.
|
|
13
|
+
*
|
|
14
|
+
* The layout decides which entries are visible (the filter and collapsed groups), how each
|
|
15
|
+
* entry breaks into lines and rows, and which values are expanded. It works from the store's
|
|
16
|
+
* change notifications, and does the pending work when `sync` is called, so any number of
|
|
17
|
+
* messages between two frames costs one update.
|
|
18
|
+
*/
|
|
19
|
+
export declare class LogLayout {
|
|
20
|
+
private readonly store;
|
|
21
|
+
private options;
|
|
22
|
+
private optionsVersion;
|
|
23
|
+
private layoutVersion;
|
|
24
|
+
private columns;
|
|
25
|
+
private filter;
|
|
26
|
+
private visible;
|
|
27
|
+
private visibleStart;
|
|
28
|
+
private readonly rowIndex;
|
|
29
|
+
private readonly layouts;
|
|
30
|
+
private readonly rowCounts;
|
|
31
|
+
private readonly expansions;
|
|
32
|
+
/** 1 where the row count of the visible entry at the same position is only an estimate. */
|
|
33
|
+
private stale;
|
|
34
|
+
private staleCount;
|
|
35
|
+
private positions;
|
|
36
|
+
private lastSyncedId;
|
|
37
|
+
private needsRebuild;
|
|
38
|
+
private dirty;
|
|
39
|
+
private widest;
|
|
40
|
+
private readonly unsubscribe;
|
|
41
|
+
constructor(store: LogStore, options?: Partial<LayoutOptions>);
|
|
42
|
+
/** The number of rows of all visible entries. Call `sync` first. */
|
|
43
|
+
get rowCount(): number;
|
|
44
|
+
/** The number of visible entries. Call `sync` first. */
|
|
45
|
+
get visibleCount(): number;
|
|
46
|
+
/** The widest row seen, in cells, including indentation. */
|
|
47
|
+
get maxCells(): number;
|
|
48
|
+
/** The number of visible entries whose row count is an estimate. See `measurePending`. */
|
|
49
|
+
get pendingCount(): number;
|
|
50
|
+
/**
|
|
51
|
+
* Increases whenever the first row of an entry that was already visible may have moved: after
|
|
52
|
+
* a rebuild, when entries are dropped from the front, and when a row count changes. Appending
|
|
53
|
+
* entries at the end does not change it. A viewer compares it between frames to keep the same
|
|
54
|
+
* entry in view.
|
|
55
|
+
*/
|
|
56
|
+
get positionsVersion(): number;
|
|
57
|
+
/** Whether the store changed since the last `sync`. */
|
|
58
|
+
get isDirty(): boolean;
|
|
59
|
+
getOptions(): Readonly<LayoutOptions>;
|
|
60
|
+
setOptions(options: Partial<LayoutOptions>): void;
|
|
61
|
+
/** Sets the number of columns rows wrap into. */
|
|
62
|
+
setColumns(columns: number): void;
|
|
63
|
+
/** Sets which entries are visible. Returns the compiled filter, which reports a bad pattern. */
|
|
64
|
+
setFilter(filter: LogFilter | null): CompiledFilter;
|
|
65
|
+
/** The compiled filter in use. */
|
|
66
|
+
getFilter(): CompiledFilter;
|
|
67
|
+
/**
|
|
68
|
+
* Applies pending store changes. Returns whether anything changed.
|
|
69
|
+
*
|
|
70
|
+
* `budget` limits how many entries are laid out exactly. Past it, an entry gets an estimated
|
|
71
|
+
* row count, based on its previous layout when it had one, and is left for `measureAround`
|
|
72
|
+
* and `measurePending`. The default lays out every entry exactly.
|
|
73
|
+
*/
|
|
74
|
+
sync(budget?: number): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Lays out exactly the entries around one entry: `rowsBefore` rows of the entries before it,
|
|
77
|
+
* and `rowsAfter` rows starting with it. Pass `null` to start from the last visible entry.
|
|
78
|
+
* Call it for the part of the log on screen before reading its rows. Returns whether a row
|
|
79
|
+
* count changed.
|
|
80
|
+
*/
|
|
81
|
+
measureAround(entryId: number | null, rowsBefore: number, rowsAfter: number): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Lays out exactly up to `budget` entries whose row count is still an estimate, nearest to an
|
|
84
|
+
* entry first, or to the end of the log when `entryId` is `null`. Call it in small pieces of
|
|
85
|
+
* work until `pendingCount` is 0. Returns whether a row count changed.
|
|
86
|
+
*/
|
|
87
|
+
measurePending(budget: number, entryId?: number | null): boolean;
|
|
88
|
+
/** Returns rows starting at a row index. Call `sync` first. */
|
|
89
|
+
getRows(start: number, count: number): VisualRow[];
|
|
90
|
+
/** Returns the entry that holds a row and the row's index within that entry. */
|
|
91
|
+
locateRow(row: number): {
|
|
92
|
+
entry: LogEntry;
|
|
93
|
+
entryRow: number;
|
|
94
|
+
} | null;
|
|
95
|
+
/** Returns the number of rows of a visible entry, or 0 when it is not visible. */
|
|
96
|
+
rowsOf(entryId: number): number;
|
|
97
|
+
/** Returns the entry at a visible position, where 0 is the oldest visible entry. */
|
|
98
|
+
entryAt(index: number): LogEntry | undefined;
|
|
99
|
+
/** Returns the visible position of an entry, or -1 when it is not visible. */
|
|
100
|
+
indexOf(entryId: number): number;
|
|
101
|
+
/** Returns the first row of an entry, or -1 when it is not visible. */
|
|
102
|
+
rowOfEntry(entryId: number): number;
|
|
103
|
+
/** Returns whether the value at a path of an entry is expanded. */
|
|
104
|
+
isExpanded(entry: LogEntry, path: string): boolean;
|
|
105
|
+
/** Runs the action of a clicked span. */
|
|
106
|
+
runAction(entryId: number, action: LineAction): void;
|
|
107
|
+
/** Expands or collapses the value at a path of an entry. */
|
|
108
|
+
setExpanded(entry: LogEntry, path: string, expanded: boolean): void;
|
|
109
|
+
/**
|
|
110
|
+
* Returns the text position under a row and a column of the content area. The position
|
|
111
|
+
* snaps to the nearest boundary between clusters.
|
|
112
|
+
*/
|
|
113
|
+
positionAt(row: number, column: number): TextPosition | null;
|
|
114
|
+
/**
|
|
115
|
+
* Returns the start and end of the word at a position: a run of letters, digits, marks and
|
|
116
|
+
* underscores. On any other character, the range covers that character alone.
|
|
117
|
+
*/
|
|
118
|
+
wordAt(position: TextPosition): [TextPosition, TextPosition] | null;
|
|
119
|
+
/** Returns the text between two positions, one line of the output per logical line. */
|
|
120
|
+
getText(from: TextPosition, to: TextPosition): string;
|
|
121
|
+
/** Returns the whole text of the visible entries. */
|
|
122
|
+
getAllText(): string;
|
|
123
|
+
/** Stops listening to the store. */
|
|
124
|
+
dispose(): void;
|
|
125
|
+
private onStoreChange;
|
|
126
|
+
private invalidateRows;
|
|
127
|
+
private rebuild;
|
|
128
|
+
/** Adds a visible entry with its exact row count, or an estimate once the budget is spent. */
|
|
129
|
+
private pushVisible;
|
|
130
|
+
/**
|
|
131
|
+
* Guesses the row count of an entry that was not laid out at the current width: its previous
|
|
132
|
+
* row count scaled by the change in width, and never fewer rows than it has lines.
|
|
133
|
+
*/
|
|
134
|
+
private estimateRows;
|
|
135
|
+
/** Lays out the visible entry at an index exactly, if it is not already. Returns its rows. */
|
|
136
|
+
private measureIndex;
|
|
137
|
+
private isVisible;
|
|
138
|
+
private stateKeyOf;
|
|
139
|
+
private countRows;
|
|
140
|
+
/**
|
|
141
|
+
* Counts the rows of the most common entry, one line of plain ASCII text, without building and
|
|
142
|
+
* caching its layout. Returns `null` for any other entry.
|
|
143
|
+
*/
|
|
144
|
+
private countPlainRows;
|
|
145
|
+
private layoutOf;
|
|
146
|
+
private buildRuns;
|
|
147
|
+
private textInCells;
|
|
148
|
+
private firstIndexFrom;
|
|
149
|
+
private forget;
|
|
150
|
+
private compactVisible;
|
|
151
|
+
private pruneCaches;
|
|
152
|
+
}
|