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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The legacy encoding a browser assumes for a page in each language, used when a file is not
|
|
3
|
+
* valid UTF-8. The list is the "suggested default encoding" table of the HTML Standard.
|
|
4
|
+
*/
|
|
5
|
+
const LEGACY_ENCODINGS = [
|
|
6
|
+
[/^ko\b/i, 'euc-kr'],
|
|
7
|
+
[/^ja\b/i, 'shift_jis'],
|
|
8
|
+
[/^zh-(hant|hk|mo|tw)\b/i, 'big5'],
|
|
9
|
+
[/^zh\b/i, 'gbk'],
|
|
10
|
+
[/^(ba|be|bg|kk|ky|mk|ru|sah|sr|tg|tt|uk)\b/i, 'windows-1251'],
|
|
11
|
+
[/^(cs|hr|sk)\b/i, 'windows-1250'],
|
|
12
|
+
[/^(hu|pl|sl)\b/i, 'iso-8859-2'],
|
|
13
|
+
[/^el\b/i, 'iso-8859-7'],
|
|
14
|
+
[/^(et|lt|lv)\b/i, 'windows-1257'],
|
|
15
|
+
[/^(ar|fa)\b/i, 'windows-1256'],
|
|
16
|
+
[/^he\b/i, 'windows-1255'],
|
|
17
|
+
[/^(az|ku|tr)\b/i, 'windows-1254'],
|
|
18
|
+
[/^th\b/i, 'windows-874'],
|
|
19
|
+
[/^vi\b/i, 'windows-1258']
|
|
20
|
+
];
|
|
21
|
+
/** Returns the legacy encoding for a language tag, such as `euc-kr` for `ko-KR`. */
|
|
22
|
+
export const legacyEncodingFor = (locale) => {
|
|
23
|
+
for (const [pattern, encoding] of LEGACY_ENCODINGS) {
|
|
24
|
+
if (locale && pattern.test(locale)) {
|
|
25
|
+
return encoding;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return 'windows-1252';
|
|
29
|
+
};
|
|
30
|
+
/** Returns the encoding a byte order mark at the start of the bytes announces, if any. */
|
|
31
|
+
export const encodingFromBom = (bytes) => {
|
|
32
|
+
if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
|
|
33
|
+
return 'utf-8';
|
|
34
|
+
}
|
|
35
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
36
|
+
return 'utf-16le';
|
|
37
|
+
}
|
|
38
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
39
|
+
return 'utf-16be';
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
/** Returns whether the bytes are valid UTF-8. A sequence cut at the end still counts as valid. */
|
|
44
|
+
export const isValidUtf8 = (bytes) => {
|
|
45
|
+
try {
|
|
46
|
+
new TextDecoder('utf-8', { fatal: true }).decode(bytes, { stream: true });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Picks the encoding of a file from its first bytes: the byte order mark if there is one,
|
|
55
|
+
* then UTF-8 if the bytes are valid UTF-8, and otherwise `fallback`.
|
|
56
|
+
*/
|
|
57
|
+
export const detectEncoding = (bytes, fallback) => {
|
|
58
|
+
return encodingFromBom(bytes) ?? (isValidUtf8(bytes) ? 'utf-8' : fallback);
|
|
59
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { LogStore } from '../../core/store.js';
|
|
2
|
+
import type { LogLevel } from '../../core/types.js';
|
|
3
|
+
/** The part of `FileSystemFileHandle` that following a file needs. */
|
|
4
|
+
export interface FileHandleLike {
|
|
5
|
+
getFile(): Promise<Blob & {
|
|
6
|
+
lastModified?: number;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
export interface FollowTextOptions {
|
|
10
|
+
/** Milliseconds between checks for new data. */
|
|
11
|
+
interval?: number;
|
|
12
|
+
/** The encoding, or `auto` to detect it from the first bytes. */
|
|
13
|
+
encoding?: string;
|
|
14
|
+
/** The encoding `auto` falls back to when the file is not UTF-8. */
|
|
15
|
+
fallbackEncoding?: string;
|
|
16
|
+
/** The level of every line. */
|
|
17
|
+
level?: LogLevel;
|
|
18
|
+
/** Whether ANSI escape codes become styles. Defaults to `true`. */
|
|
19
|
+
ansi?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Called when the file was replaced rather than appended to: it got shorter, or it kept its
|
|
22
|
+
* size but was modified. The file is then read again from the start.
|
|
23
|
+
*/
|
|
24
|
+
onReset?: () => void;
|
|
25
|
+
/** Called when reading fails, for example because permission was withdrawn. */
|
|
26
|
+
onError?: (error: unknown) => void;
|
|
27
|
+
}
|
|
28
|
+
/** Controls a followed file. */
|
|
29
|
+
export interface FollowHandle {
|
|
30
|
+
/** Stops checking the file and writes any unfinished last line. */
|
|
31
|
+
stop(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Resolves after the first read with the number of lines it added, or rejects when the first
|
|
34
|
+
* read fails. Checking goes on either way; later failures go to `onError`.
|
|
35
|
+
*/
|
|
36
|
+
ready: Promise<number>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Reads a file and keeps adding the lines appended to it, like `tail -f`.
|
|
40
|
+
*
|
|
41
|
+
* This needs a file handle from the File System Access API (`showOpenFilePicker`), which only
|
|
42
|
+
* Chromium-based browsers provide. Every check asks the handle for a fresh copy of the file
|
|
43
|
+
* and reads what was added since the last one. A file picked with `<input type="file">` is a
|
|
44
|
+
* snapshot that cannot be followed; read it once with `readTextFile` instead.
|
|
45
|
+
*
|
|
46
|
+
* When the file gets shorter, or keeps its size but changes, it was replaced rather than
|
|
47
|
+
* appended to, so it is read again from the start and `onReset` is called.
|
|
48
|
+
*/
|
|
49
|
+
export declare const followTextFile: (handle: FileHandleLike, store: LogStore, options?: FollowTextOptions) => FollowHandle;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { detectEncoding, legacyEncodingFor } from './encoding.js';
|
|
2
|
+
import { TextLineWriter } from './read-file.js';
|
|
3
|
+
const DEFAULT_INTERVAL = 1000;
|
|
4
|
+
const browserLanguage = () => {
|
|
5
|
+
return globalThis.navigator?.language;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Reads a file and keeps adding the lines appended to it, like `tail -f`.
|
|
9
|
+
*
|
|
10
|
+
* This needs a file handle from the File System Access API (`showOpenFilePicker`), which only
|
|
11
|
+
* Chromium-based browsers provide. Every check asks the handle for a fresh copy of the file
|
|
12
|
+
* and reads what was added since the last one. A file picked with `<input type="file">` is a
|
|
13
|
+
* snapshot that cannot be followed; read it once with `readTextFile` instead.
|
|
14
|
+
*
|
|
15
|
+
* When the file gets shorter, or keeps its size but changes, it was replaced rather than
|
|
16
|
+
* appended to, so it is read again from the start and `onReset` is called.
|
|
17
|
+
*/
|
|
18
|
+
export const followTextFile = (handle, store, options = {}) => {
|
|
19
|
+
const interval = options.interval ?? DEFAULT_INTERVAL;
|
|
20
|
+
const writerOptions = { level: options.level, ansi: options.ansi };
|
|
21
|
+
let writer = new TextLineWriter(store, writerOptions);
|
|
22
|
+
let decoder = null;
|
|
23
|
+
let offset = 0;
|
|
24
|
+
let lastModified;
|
|
25
|
+
let stopped = false;
|
|
26
|
+
let timer;
|
|
27
|
+
let settleReady = null;
|
|
28
|
+
const ready = new Promise((resolve, reject) => {
|
|
29
|
+
settleReady = { resolve, reject };
|
|
30
|
+
});
|
|
31
|
+
// A caller that never awaits `ready` should not see an unhandled rejection.
|
|
32
|
+
ready.catch(() => undefined);
|
|
33
|
+
const reset = () => {
|
|
34
|
+
writer.write(decoder?.decode() ?? '');
|
|
35
|
+
writer.flush();
|
|
36
|
+
writer = new TextLineWriter(store, writerOptions);
|
|
37
|
+
decoder = null;
|
|
38
|
+
offset = 0;
|
|
39
|
+
options.onReset?.();
|
|
40
|
+
};
|
|
41
|
+
const check = async () => {
|
|
42
|
+
const file = await handle.getFile();
|
|
43
|
+
if (stopped) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const replaced = file.size < offset ||
|
|
47
|
+
(file.size === offset &&
|
|
48
|
+
offset > 0 &&
|
|
49
|
+
file.lastModified !== undefined &&
|
|
50
|
+
lastModified !== undefined &&
|
|
51
|
+
file.lastModified !== lastModified);
|
|
52
|
+
lastModified = file.lastModified;
|
|
53
|
+
if (replaced) {
|
|
54
|
+
reset();
|
|
55
|
+
}
|
|
56
|
+
if (file.size === offset) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const bytes = new Uint8Array(await file.slice(offset).arrayBuffer());
|
|
60
|
+
if (stopped) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!decoder) {
|
|
64
|
+
const requested = options.encoding ?? 'auto';
|
|
65
|
+
const encoding = requested === 'auto'
|
|
66
|
+
? detectEncoding(bytes, options.fallbackEncoding ?? legacyEncodingFor(browserLanguage()))
|
|
67
|
+
: requested;
|
|
68
|
+
decoder = new TextDecoder(encoding);
|
|
69
|
+
}
|
|
70
|
+
writer.write(decoder.decode(bytes, { stream: true }));
|
|
71
|
+
offset += bytes.byteLength;
|
|
72
|
+
};
|
|
73
|
+
const loop = async () => {
|
|
74
|
+
if (stopped) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await check();
|
|
79
|
+
settleReady?.resolve(writer.lines);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (settleReady) {
|
|
83
|
+
settleReady.reject(error);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
options.onError?.(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
settleReady = null;
|
|
90
|
+
if (!stopped) {
|
|
91
|
+
timer = setTimeout(() => {
|
|
92
|
+
void loop();
|
|
93
|
+
}, interval);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
void loop();
|
|
97
|
+
return {
|
|
98
|
+
ready,
|
|
99
|
+
stop: () => {
|
|
100
|
+
if (stopped) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
stopped = true;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
writer.write(decoder?.decode() ?? '');
|
|
106
|
+
writer.flush();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { LogStore } from '../../core/store.js';
|
|
2
|
+
import type { LogLevel } from '../../core/types.js';
|
|
3
|
+
export interface ReadTextOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The encoding of the file, such as `utf-8` or `euc-kr`. `auto` reads the byte order mark,
|
|
6
|
+
* then tries UTF-8, then falls back to `fallbackEncoding`.
|
|
7
|
+
*/
|
|
8
|
+
encoding?: string;
|
|
9
|
+
/**
|
|
10
|
+
* The encoding used by `auto` when the file is not UTF-8. Defaults to the legacy encoding of
|
|
11
|
+
* the browser language, such as `euc-kr` for Korean.
|
|
12
|
+
*/
|
|
13
|
+
fallbackEncoding?: string;
|
|
14
|
+
/** The level of every line. */
|
|
15
|
+
level?: LogLevel;
|
|
16
|
+
/** Whether ANSI escape codes become styles. Defaults to `true`. */
|
|
17
|
+
ansi?: boolean;
|
|
18
|
+
/** Bytes read at a time. */
|
|
19
|
+
chunkSize?: number;
|
|
20
|
+
/** Stops reading when aborted. The lines read so far stay in the store. */
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
/** Called after every chunk with the bytes read so far and the total size. */
|
|
23
|
+
onProgress?: (loaded: number, total: number) => void;
|
|
24
|
+
}
|
|
25
|
+
export interface ReadTextResult {
|
|
26
|
+
/** The number of lines added. */
|
|
27
|
+
lines: number;
|
|
28
|
+
/** The number of bytes read. */
|
|
29
|
+
bytes: number;
|
|
30
|
+
/** The encoding used. */
|
|
31
|
+
encoding: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Turns decoded text into store entries one line at a time, keeping partial lines and the ANSI
|
|
35
|
+
* style between chunks.
|
|
36
|
+
*/
|
|
37
|
+
export declare class TextLineWriter {
|
|
38
|
+
private readonly store;
|
|
39
|
+
private readonly options;
|
|
40
|
+
private readonly splitter;
|
|
41
|
+
private readonly parser;
|
|
42
|
+
private count;
|
|
43
|
+
constructor(store: LogStore, options?: {
|
|
44
|
+
level?: LogLevel;
|
|
45
|
+
ansi?: boolean;
|
|
46
|
+
});
|
|
47
|
+
/** The number of lines written. */
|
|
48
|
+
get lines(): number;
|
|
49
|
+
/** Whether an unfinished line is waiting for its line break. */
|
|
50
|
+
get hasPending(): boolean;
|
|
51
|
+
write(text: string): void;
|
|
52
|
+
/** Writes the unfinished last line, if any. */
|
|
53
|
+
flush(): void;
|
|
54
|
+
private add;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads a text file, such as one picked with `<input type="file">` or dropped on the page, and
|
|
58
|
+
* adds one entry per line.
|
|
59
|
+
*
|
|
60
|
+
* The file is read in chunks, so a large file does not have to fit in memory as one string.
|
|
61
|
+
* The store's `maxEntries` still decides how many lines are kept.
|
|
62
|
+
*/
|
|
63
|
+
export declare const readTextFile: (file: Blob, store: LogStore, options?: ReadTextOptions) => Promise<ReadTextResult>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AnsiParser } from '../../core/text/ansi.js';
|
|
2
|
+
import { LineSplitter } from '../../core/text/line-splitter.js';
|
|
3
|
+
import { detectEncoding, legacyEncodingFor } from './encoding.js';
|
|
4
|
+
const DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
5
|
+
const DETECTION_BYTES = 64 * 1024;
|
|
6
|
+
const browserLanguage = () => {
|
|
7
|
+
return globalThis.navigator?.language;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Turns decoded text into store entries one line at a time, keeping partial lines and the ANSI
|
|
11
|
+
* style between chunks.
|
|
12
|
+
*/
|
|
13
|
+
export class TextLineWriter {
|
|
14
|
+
store;
|
|
15
|
+
options;
|
|
16
|
+
splitter = new LineSplitter();
|
|
17
|
+
parser;
|
|
18
|
+
count = 0;
|
|
19
|
+
constructor(store, options = {}) {
|
|
20
|
+
this.store = store;
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.parser = options.ansi === false ? null : new AnsiParser();
|
|
23
|
+
}
|
|
24
|
+
/** The number of lines written. */
|
|
25
|
+
get lines() {
|
|
26
|
+
return this.count;
|
|
27
|
+
}
|
|
28
|
+
/** Whether an unfinished line is waiting for its line break. */
|
|
29
|
+
get hasPending() {
|
|
30
|
+
return this.splitter.hasPending;
|
|
31
|
+
}
|
|
32
|
+
write(text) {
|
|
33
|
+
this.add(this.splitter.push(text));
|
|
34
|
+
}
|
|
35
|
+
/** Writes the unfinished last line, if any. */
|
|
36
|
+
flush() {
|
|
37
|
+
this.add(this.splitter.flush());
|
|
38
|
+
}
|
|
39
|
+
add(lines) {
|
|
40
|
+
if (lines.length === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const level = this.options.level ?? 'log';
|
|
44
|
+
const entries = lines.map((line) => ({
|
|
45
|
+
level,
|
|
46
|
+
parts: this.parser ? this.parser.parse(line) : [{ type: 'text', text: line }]
|
|
47
|
+
}));
|
|
48
|
+
this.store.append(entries);
|
|
49
|
+
this.count += lines.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads a text file, such as one picked with `<input type="file">` or dropped on the page, and
|
|
54
|
+
* adds one entry per line.
|
|
55
|
+
*
|
|
56
|
+
* The file is read in chunks, so a large file does not have to fit in memory as one string.
|
|
57
|
+
* The store's `maxEntries` still decides how many lines are kept.
|
|
58
|
+
*/
|
|
59
|
+
export const readTextFile = async (file, store, options = {}) => {
|
|
60
|
+
const chunkSize = options.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
61
|
+
const head = new Uint8Array(await file.slice(0, DETECTION_BYTES).arrayBuffer());
|
|
62
|
+
const requested = options.encoding ?? 'auto';
|
|
63
|
+
const encoding = requested === 'auto'
|
|
64
|
+
? detectEncoding(head, options.fallbackEncoding ?? legacyEncodingFor(browserLanguage()))
|
|
65
|
+
: requested;
|
|
66
|
+
const decoder = new TextDecoder(encoding);
|
|
67
|
+
const writer = new TextLineWriter(store, { level: options.level, ansi: options.ansi });
|
|
68
|
+
let offset = 0;
|
|
69
|
+
while (offset < file.size) {
|
|
70
|
+
if (options.signal?.aborted) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
const end = Math.min(file.size, offset + chunkSize);
|
|
74
|
+
const bytes = new Uint8Array(await file.slice(offset, end).arrayBuffer());
|
|
75
|
+
writer.write(decoder.decode(bytes, { stream: true }));
|
|
76
|
+
offset = end;
|
|
77
|
+
options.onProgress?.(offset, file.size);
|
|
78
|
+
}
|
|
79
|
+
writer.write(decoder.decode());
|
|
80
|
+
writer.flush();
|
|
81
|
+
return { lines: writer.lines, bytes: offset, encoding };
|
|
82
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Path data for the toolbar icons, drawn on a 16 by 16 grid with a 1.5 stroke. */
|
|
2
|
+
declare const ICON_PATHS: {
|
|
3
|
+
readonly follow: "M8 2.75v7.5M4.75 7 8 10.25 11.25 7M3.5 13.25h9";
|
|
4
|
+
readonly clear: "M8 2.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5ZM4.3 11.7l7.4-7.4";
|
|
5
|
+
readonly top: "M3.5 2.75h9M8 13.25v-7.5M4.75 9 8 5.75 11.25 9";
|
|
6
|
+
readonly bottom: "M3.5 13.25h9M8 2.75v7.5M4.75 7 8 10.25 11.25 7";
|
|
7
|
+
readonly wrap: "M2.75 4h10.5M2.75 8h8.5a2 2 0 0 1 0 4H8.5M10 10.5 8.5 12l1.5 1.5M2.75 12h3";
|
|
8
|
+
readonly search: "M7 2.75a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM10.25 10.25l3 3";
|
|
9
|
+
readonly arrowDown: "M8 3.25v9.5M4.25 9 8 12.75 11.75 9";
|
|
10
|
+
};
|
|
11
|
+
export type IconName = keyof typeof ICON_PATHS;
|
|
12
|
+
/** Creates an inline SVG icon. Icons are decorative; the control around them carries the name. */
|
|
13
|
+
export declare const createIcon: (ownerDocument: Document, name: IconName) => SVGSVGElement;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
2
|
+
/** Path data for the toolbar icons, drawn on a 16 by 16 grid with a 1.5 stroke. */
|
|
3
|
+
const ICON_PATHS = {
|
|
4
|
+
follow: 'M8 2.75v7.5M4.75 7 8 10.25 11.25 7M3.5 13.25h9',
|
|
5
|
+
clear: 'M8 2.75a5.25 5.25 0 1 0 0 10.5 5.25 5.25 0 0 0 0-10.5ZM4.3 11.7l7.4-7.4',
|
|
6
|
+
top: 'M3.5 2.75h9M8 13.25v-7.5M4.75 9 8 5.75 11.25 9',
|
|
7
|
+
bottom: 'M3.5 13.25h9M8 2.75v7.5M4.75 7 8 10.25 11.25 7',
|
|
8
|
+
wrap: 'M2.75 4h10.5M2.75 8h8.5a2 2 0 0 1 0 4H8.5M10 10.5 8.5 12l1.5 1.5M2.75 12h3',
|
|
9
|
+
search: 'M7 2.75a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM10.25 10.25l3 3',
|
|
10
|
+
arrowDown: 'M8 3.25v9.5M4.25 9 8 12.75 11.75 9'
|
|
11
|
+
};
|
|
12
|
+
/** Creates an inline SVG icon. Icons are decorative; the control around them carries the name. */
|
|
13
|
+
export const createIcon = (ownerDocument, name) => {
|
|
14
|
+
const svg = ownerDocument.createElementNS(SVG_NAMESPACE, 'svg');
|
|
15
|
+
const path = ownerDocument.createElementNS(SVG_NAMESPACE, 'path');
|
|
16
|
+
svg.setAttribute('viewBox', '0 0 16 16');
|
|
17
|
+
svg.setAttribute('width', '16');
|
|
18
|
+
svg.setAttribute('height', '16');
|
|
19
|
+
svg.setAttribute('fill', 'none');
|
|
20
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
21
|
+
svg.setAttribute('focusable', 'false');
|
|
22
|
+
svg.classList.add('lognal-icon');
|
|
23
|
+
path.setAttribute('d', ICON_PATHS[name]);
|
|
24
|
+
path.setAttribute('stroke', 'currentColor');
|
|
25
|
+
path.setAttribute('stroke-width', '1.5');
|
|
26
|
+
path.setAttribute('stroke-linecap', 'round');
|
|
27
|
+
path.setAttribute('stroke-linejoin', 'round');
|
|
28
|
+
svg.append(path);
|
|
29
|
+
return svg;
|
|
30
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface InputLineOptions {
|
|
2
|
+
/** Called with the text when the user presses Enter. */
|
|
3
|
+
onSubmit: (text: string) => void;
|
|
4
|
+
/** The prompt drawn before the input. */
|
|
5
|
+
prompt: string;
|
|
6
|
+
placeholder: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** How many past commands ArrowUp and ArrowDown go through. */
|
|
9
|
+
historySize: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The line where the user types commands.
|
|
13
|
+
*
|
|
14
|
+
* It is a real `<textarea>`, so the browser handles focus, the caret, selection, paste and
|
|
15
|
+
* input method composition. Enter submits and Shift+Enter adds a line.
|
|
16
|
+
*
|
|
17
|
+
* Enter is ignored while an IME composition is open. Safari through version 26 fires
|
|
18
|
+
* `compositionend` before the `keydown` of the key that commits the composition, so that
|
|
19
|
+
* `keydown` reports `isComposing` as false. The input therefore also checks key code 229 and
|
|
20
|
+
* keeps treating the composition as open until the task after `compositionend`.
|
|
21
|
+
*/
|
|
22
|
+
export declare class InputLine {
|
|
23
|
+
private options;
|
|
24
|
+
readonly element: HTMLFormElement;
|
|
25
|
+
private readonly textarea;
|
|
26
|
+
private readonly promptElement;
|
|
27
|
+
private readonly history;
|
|
28
|
+
private historyIndex;
|
|
29
|
+
private draft;
|
|
30
|
+
private composing;
|
|
31
|
+
private compositionTimer;
|
|
32
|
+
constructor(ownerDocument: Document, options: InputLineOptions);
|
|
33
|
+
get value(): string;
|
|
34
|
+
setOptions(options: InputLineOptions): void;
|
|
35
|
+
focus(): void;
|
|
36
|
+
dispose(): void;
|
|
37
|
+
private submit;
|
|
38
|
+
private isComposing;
|
|
39
|
+
private showHistory;
|
|
40
|
+
private resize;
|
|
41
|
+
private readonly onFormSubmit;
|
|
42
|
+
private readonly onKeyDown;
|
|
43
|
+
private readonly onInput;
|
|
44
|
+
private readonly onCompositionStart;
|
|
45
|
+
private readonly onCompositionEnd;
|
|
46
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/** The most lines the input grows to before it scrolls. */
|
|
2
|
+
const MAX_VISIBLE_LINES = 6;
|
|
3
|
+
/** Key code browsers report for a key press that belongs to an IME composition. */
|
|
4
|
+
const IME_KEY_CODE = 229;
|
|
5
|
+
/**
|
|
6
|
+
* The line where the user types commands.
|
|
7
|
+
*
|
|
8
|
+
* It is a real `<textarea>`, so the browser handles focus, the caret, selection, paste and
|
|
9
|
+
* input method composition. Enter submits and Shift+Enter adds a line.
|
|
10
|
+
*
|
|
11
|
+
* Enter is ignored while an IME composition is open. Safari through version 26 fires
|
|
12
|
+
* `compositionend` before the `keydown` of the key that commits the composition, so that
|
|
13
|
+
* `keydown` reports `isComposing` as false. The input therefore also checks key code 229 and
|
|
14
|
+
* keeps treating the composition as open until the task after `compositionend`.
|
|
15
|
+
*/
|
|
16
|
+
export class InputLine {
|
|
17
|
+
options;
|
|
18
|
+
element;
|
|
19
|
+
textarea;
|
|
20
|
+
promptElement;
|
|
21
|
+
history = [];
|
|
22
|
+
historyIndex = -1;
|
|
23
|
+
draft = '';
|
|
24
|
+
composing = false;
|
|
25
|
+
compositionTimer;
|
|
26
|
+
constructor(ownerDocument, options) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.element = ownerDocument.createElement('form');
|
|
29
|
+
this.element.className = 'lognal-input';
|
|
30
|
+
this.promptElement = ownerDocument.createElement('span');
|
|
31
|
+
this.promptElement.className = 'lognal-input-prompt';
|
|
32
|
+
this.promptElement.setAttribute('aria-hidden', 'true');
|
|
33
|
+
this.textarea = ownerDocument.createElement('textarea');
|
|
34
|
+
this.textarea.className = 'lognal-input-field';
|
|
35
|
+
this.textarea.rows = 1;
|
|
36
|
+
this.textarea.spellcheck = false;
|
|
37
|
+
this.textarea.setAttribute('autocomplete', 'off');
|
|
38
|
+
this.textarea.setAttribute('autocapitalize', 'off');
|
|
39
|
+
this.textarea.setAttribute('autocorrect', 'off');
|
|
40
|
+
this.element.append(this.promptElement, this.textarea);
|
|
41
|
+
this.setOptions(options);
|
|
42
|
+
this.element.addEventListener('submit', this.onFormSubmit);
|
|
43
|
+
this.textarea.addEventListener('keydown', this.onKeyDown);
|
|
44
|
+
this.textarea.addEventListener('input', this.onInput);
|
|
45
|
+
this.textarea.addEventListener('compositionstart', this.onCompositionStart);
|
|
46
|
+
this.textarea.addEventListener('compositionend', this.onCompositionEnd);
|
|
47
|
+
}
|
|
48
|
+
get value() {
|
|
49
|
+
return this.textarea.value;
|
|
50
|
+
}
|
|
51
|
+
setOptions(options) {
|
|
52
|
+
this.options = options;
|
|
53
|
+
this.promptElement.textContent = options.prompt;
|
|
54
|
+
this.textarea.placeholder = options.placeholder;
|
|
55
|
+
this.textarea.setAttribute('aria-label', options.label);
|
|
56
|
+
}
|
|
57
|
+
focus() {
|
|
58
|
+
this.textarea.focus();
|
|
59
|
+
}
|
|
60
|
+
dispose() {
|
|
61
|
+
clearTimeout(this.compositionTimer);
|
|
62
|
+
this.element.remove();
|
|
63
|
+
}
|
|
64
|
+
submit() {
|
|
65
|
+
const text = this.textarea.value;
|
|
66
|
+
if (!text.trim()) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (this.history[this.history.length - 1] !== text) {
|
|
70
|
+
this.history.push(text);
|
|
71
|
+
if (this.history.length > this.options.historySize) {
|
|
72
|
+
this.history.shift();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.historyIndex = -1;
|
|
76
|
+
this.draft = '';
|
|
77
|
+
this.textarea.value = '';
|
|
78
|
+
this.resize();
|
|
79
|
+
this.options.onSubmit(text);
|
|
80
|
+
}
|
|
81
|
+
isComposing(event) {
|
|
82
|
+
return this.composing || event.isComposing || event.keyCode === IME_KEY_CODE;
|
|
83
|
+
}
|
|
84
|
+
showHistory(index) {
|
|
85
|
+
if (this.historyIndex === -1) {
|
|
86
|
+
this.draft = this.textarea.value;
|
|
87
|
+
}
|
|
88
|
+
this.historyIndex = index;
|
|
89
|
+
this.textarea.value = index === -1 ? this.draft : this.history[index];
|
|
90
|
+
this.resize();
|
|
91
|
+
const end = this.textarea.value.length;
|
|
92
|
+
this.textarea.setSelectionRange(end, end);
|
|
93
|
+
}
|
|
94
|
+
resize() {
|
|
95
|
+
const lines = Math.min(MAX_VISIBLE_LINES, this.textarea.value.split('\n').length);
|
|
96
|
+
this.textarea.rows = Math.max(1, lines);
|
|
97
|
+
}
|
|
98
|
+
onFormSubmit = (event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
this.submit();
|
|
101
|
+
};
|
|
102
|
+
onKeyDown = (event) => {
|
|
103
|
+
if (this.isComposing(event)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const { selectionStart, selectionEnd, value } = this.textarea;
|
|
107
|
+
const caretCollapsed = selectionStart === selectionEnd;
|
|
108
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
this.submit();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (event.key === 'ArrowUp' &&
|
|
114
|
+
caretCollapsed &&
|
|
115
|
+
!value.slice(0, selectionStart).includes('\n')) {
|
|
116
|
+
const next = this.historyIndex === -1 ? this.history.length - 1 : this.historyIndex - 1;
|
|
117
|
+
if (next >= 0) {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
this.showHistory(next);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (event.key === 'ArrowDown' && caretCollapsed && !value.slice(selectionEnd).includes('\n')) {
|
|
124
|
+
if (this.historyIndex === -1) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
this.showHistory(this.historyIndex + 1 < this.history.length ? this.historyIndex + 1 : -1);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
onInput = () => {
|
|
132
|
+
this.resize();
|
|
133
|
+
};
|
|
134
|
+
onCompositionStart = () => {
|
|
135
|
+
clearTimeout(this.compositionTimer);
|
|
136
|
+
this.composing = true;
|
|
137
|
+
};
|
|
138
|
+
onCompositionEnd = () => {
|
|
139
|
+
clearTimeout(this.compositionTimer);
|
|
140
|
+
this.compositionTimer = setTimeout(() => {
|
|
141
|
+
this.composing = false;
|
|
142
|
+
}, 0);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The text of the viewer's controls. Every label is used as visible text or an accessible name. */
|
|
2
|
+
export interface ViewerLabels {
|
|
3
|
+
/** Accessible name of the whole viewer. */
|
|
4
|
+
viewer: string;
|
|
5
|
+
/** Accessible name of the toolbar. */
|
|
6
|
+
toolbar: string;
|
|
7
|
+
follow: string;
|
|
8
|
+
clear: string;
|
|
9
|
+
scrollToTop: string;
|
|
10
|
+
scrollToBottom: string;
|
|
11
|
+
wrap: string;
|
|
12
|
+
filter: string;
|
|
13
|
+
invalidFilter: string;
|
|
14
|
+
levels: string;
|
|
15
|
+
levelAll: string;
|
|
16
|
+
levelLog: string;
|
|
17
|
+
levelInfo: string;
|
|
18
|
+
levelWarn: string;
|
|
19
|
+
levelError: string;
|
|
20
|
+
/** Accessible name of the input line. */
|
|
21
|
+
input: string;
|
|
22
|
+
inputPlaceholder: string;
|
|
23
|
+
/** The button that appears when new entries arrive while the view is scrolled up. */
|
|
24
|
+
newLogs: string;
|
|
25
|
+
/** Accessible name of the list that mirrors the visible entries for screen readers. */
|
|
26
|
+
entryList: string;
|
|
27
|
+
following: string;
|
|
28
|
+
paused: string;
|
|
29
|
+
/** The entry count in the status bar. `format` formats a number for the locale. */
|
|
30
|
+
entries: (shown: number, total: number, format: (value: number) => string) => string;
|
|
31
|
+
}
|
|
32
|
+
export declare const EN_LABELS: ViewerLabels;
|
|
33
|
+
export declare const KO_LABELS: ViewerLabels;
|
|
34
|
+
/** Returns the built-in labels for a language tag. English is used for any other language. */
|
|
35
|
+
export declare const labelsFor: (locale: string | undefined) => ViewerLabels;
|