paseo-plugin-genui 0.0.0-stage → 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 +202 -0
- package/OVERVIEW.md +21 -0
- package/README.md +77 -2
- package/assets/fonts/Excalifont-OFL.txt +109 -0
- package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
- package/assets/icon.png +0 -0
- package/assets/icon.svg +1 -0
- package/assets/screenshot-chat.png +0 -0
- package/assets/screenshot-gallery.png +0 -0
- package/assets/screenshot-playground.png +0 -0
- package/client/components/content.tsx +468 -0
- package/client/components/data.tsx +967 -0
- package/client/components/dev.tsx +998 -0
- package/client/components/diagram.tsx +513 -0
- package/client/components/inputs.tsx +994 -0
- package/client/components/layout.tsx +239 -0
- package/client/components/media.tsx +428 -0
- package/client/components/project.tsx +641 -0
- package/client/components/widgets.tsx +836 -0
- package/client/gallery.tsx +449 -0
- package/client/live-ui.tsx +525 -0
- package/client/loaders.ts +342 -0
- package/client/markdown.tsx +172 -0
- package/client/node-renderer.tsx +149 -0
- package/client/render.tsx +186 -0
- package/client/runtime.ts +103 -0
- package/client/settings.tsx +57 -0
- package/client/web.ts +186 -0
- package/gallery/Acceptance-baseline.ui +1 -0
- package/gallery/Acceptance-checklist.ui +14 -0
- package/gallery/Acceptance-evidence.ui +9 -0
- package/gallery/Acceptance.ui +1 -0
- package/gallery/Badge.ui +3 -0
- package/gallery/Benchmark-context.ui +8 -0
- package/gallery/Benchmark-hyperfine.ui +1 -0
- package/gallery/Benchmark-runs-list.ui +9 -0
- package/gallery/Benchmark-runs.ui +1 -0
- package/gallery/Benchmark-vitest.ui +1 -0
- package/gallery/Benchmark.ui +9 -0
- package/gallery/Board-bb-tasks.ui +1 -0
- package/gallery/Board-gh-issues.ui +1 -0
- package/gallery/Board-gh-prs.ui +1 -0
- package/gallery/Board.ui +12 -0
- package/gallery/Button-destructive.ui +4 -0
- package/gallery/Button.ui +5 -0
- package/gallery/Calendar.ui +15 -0
- package/gallery/CalendarHeatmap-short.ui +11 -0
- package/gallery/CalendarHeatmap.ui +4 -0
- package/gallery/Callout.ui +4 -0
- package/gallery/Card.ui +4 -0
- package/gallery/Carousel.ui +8 -0
- package/gallery/Changes-paths.ui +1 -0
- package/gallery/Changes-uncommitted.ui +1 -0
- package/gallery/Changes.ui +1 -0
- package/gallery/Chart-area.ui +3 -0
- package/gallery/Chart-funnel.ui +3 -0
- package/gallery/Chart-heatmap.ui +11 -0
- package/gallery/Chart-pie.ui +3 -0
- package/gallery/Chart-radar.ui +3 -0
- package/gallery/Chart-scatter.ui +4 -0
- package/gallery/Chart-stacked.ui +5 -0
- package/gallery/Chart.ui +3 -0
- package/gallery/Checklist.ui +6 -0
- package/gallery/Choice.ui +8 -0
- package/gallery/Code.ui +3 -0
- package/gallery/Commits-limit.ui +1 -0
- package/gallery/Commits.ui +1 -0
- package/gallery/Compare-conditions.ui +4 -0
- package/gallery/Compare-three.ui +7 -0
- package/gallery/Compare.ui +4 -0
- package/gallery/Coverage-go.ui +1 -0
- package/gallery/Coverage-lcov.ui +1 -0
- package/gallery/Coverage-python.ui +1 -0
- package/gallery/Coverage.ui +1 -0
- package/gallery/DateInput.ui +5 -0
- package/gallery/Decision-percent.ui +10 -0
- package/gallery/Decision.ui +12 -0
- package/gallery/Dependencies-audit.ui +1 -0
- package/gallery/Dependencies-both.ui +1 -0
- package/gallery/Dependencies-list.ui +7 -0
- package/gallery/Dependencies-pip.ui +1 -0
- package/gallery/Dependencies-pnpm.ui +1 -0
- package/gallery/Dependencies.ui +1 -0
- package/gallery/Details.ui +4 -0
- package/gallery/Diagram-flow.ui +16 -0
- package/gallery/Diagram-sequence.ui +15 -0
- package/gallery/Diagram.ui +16 -0
- package/gallery/Diff.ui +3 -0
- package/gallery/Divider.ui +5 -0
- package/gallery/EditableTable.ui +9 -0
- package/gallery/Entity-list.ui +6 -0
- package/gallery/Entity.ui +8 -0
- package/gallery/Environments.ui +1 -0
- package/gallery/Estimate-points.ui +6 -0
- package/gallery/Estimate.ui +8 -0
- package/gallery/File.ui +4 -0
- package/gallery/Findings-biome.ui +1 -0
- package/gallery/Findings-eslint.ui +1 -0
- package/gallery/Findings-oxlint.ui +1 -0
- package/gallery/Findings-review.ui +1 -0
- package/gallery/Findings-ruff.ui +1 -0
- package/gallery/Findings-sarif.ui +1 -0
- package/gallery/Findings-ship.ui +11 -0
- package/gallery/Findings-tsc.ui +1 -0
- package/gallery/Findings.ui +7 -0
- package/gallery/Flashcards.ui +3 -0
- package/gallery/FollowUps.ui +4 -0
- package/gallery/Form.ui +8 -0
- package/gallery/Gallery-links.ui +8 -0
- package/gallery/Gallery-strip.ui +14 -0
- package/gallery/Gallery.ui +10 -0
- package/gallery/Gauge.ui +7 -0
- package/gallery/Grid.ui +8 -0
- package/gallery/Hotspots-photo.ui +8 -0
- package/gallery/Hotspots.ui +11 -0
- package/gallery/Icon.ui +5 -0
- package/gallery/Image.ui +4 -0
- package/gallery/ImageSearch-aspects.ui +6 -0
- package/gallery/ImageSearch.ui +4 -0
- package/gallery/Input.ui +5 -0
- package/gallery/Json-api.ui +1 -0
- package/gallery/Json.ui +8 -0
- package/gallery/KeyValue.ui +3 -0
- package/gallery/List-dots.ui +8 -0
- package/gallery/List.ui +8 -0
- package/gallery/ListEditor.ui +5 -0
- package/gallery/Logs-jsonl.ui +1 -0
- package/gallery/Logs-lines.ui +11 -0
- package/gallery/Logs-python.ui +1 -0
- package/gallery/Logs.ui +1 -0
- package/gallery/Map-city.ui +8 -0
- package/gallery/Map.ui +8 -0
- package/gallery/Metric.ui +3 -0
- package/gallery/OptionCards-multi.ui +10 -0
- package/gallery/OptionCards.ui +10 -0
- package/gallery/Pipeline-checks.ui +1 -0
- package/gallery/Pipeline-passed.ui +1 -0
- package/gallery/Pipeline-running.ui +1 -0
- package/gallery/Pipeline-stages.ui +13 -0
- package/gallery/Pipeline.ui +1 -0
- package/gallery/Plan.ui +9 -0
- package/gallery/Progress.ui +4 -0
- package/gallery/Provenance-deploy.ui +6 -0
- package/gallery/Provenance-no-receipt.ui +1 -0
- package/gallery/Provenance.ui +1 -0
- package/gallery/PullRequest.ui +1 -0
- package/gallery/Quiz.ui +6 -0
- package/gallery/Radio.ui +5 -0
- package/gallery/Rating.ui +5 -0
- package/gallery/Roadmap-sprint.ui +8 -0
- package/gallery/Roadmap-year.ui +9 -0
- package/gallery/Roadmap.ui +12 -0
- package/gallery/Row.ui +3 -0
- package/gallery/SegmentedBar-status.ui +8 -0
- package/gallery/SegmentedBar.ui +8 -0
- package/gallery/Select.ui +5 -0
- package/gallery/Slider.ui +7 -0
- package/gallery/Snippet.ui +3 -0
- package/gallery/Sources-chips.ui +4 -0
- package/gallery/Sources.ui +8 -0
- package/gallery/Sparkline.ui +6 -0
- package/gallery/Stack.ui +7 -0
- package/gallery/StackTrace-aggregate.ui +1 -0
- package/gallery/StackTrace-browser.ui +1 -0
- package/gallery/StackTrace-go.ui +1 -0
- package/gallery/StackTrace-java.ui +1 -0
- package/gallery/StackTrace-python.ui +1 -0
- package/gallery/StackTrace-rust.ui +1 -0
- package/gallery/StackTrace.ui +1 -0
- package/gallery/Status.ui +9 -0
- package/gallery/Stepper.ui +5 -0
- package/gallery/Steps.ui +8 -0
- package/gallery/Swatches-long-names.ui +4 -0
- package/gallery/Swatches-picker.ui +5 -0
- package/gallery/Swatches.ui +10 -0
- package/gallery/Table.ui +7 -0
- package/gallery/Tabs.ui +8 -0
- package/gallery/Tests-junit.ui +1 -0
- package/gallery/Tests-vitest-junit.ui +1 -0
- package/gallery/Tests-vitest.ui +1 -0
- package/gallery/Tests.ui +9 -0
- package/gallery/Text.ui +4 -0
- package/gallery/TextArea.ui +5 -0
- package/gallery/Threads-ids.ui +1 -0
- package/gallery/Threads.ui +1 -0
- package/gallery/Timeline.ui +8 -0
- package/gallery/Timer.ui +4 -0
- package/gallery/Toggle.ui +5 -0
- package/gallery/Transcript-text.ui +1 -0
- package/gallery/Transcript-untimed.ui +1 -0
- package/gallery/Transcript-whisper.ui +1 -0
- package/gallery/Transcript.ui +1 -0
- package/gallery/Tree.ui +3 -0
- package/gallery/Treemap-du.ui +1 -0
- package/gallery/Treemap-esbuild.ui +1 -0
- package/gallery/Treemap-tree.ui +7 -0
- package/gallery/Treemap.ui +18 -0
- package/gallery/Walkthrough.ui +9 -0
- package/gallery/Waterfall-har.ui +1 -0
- package/gallery/Waterfall-trace.ui +1 -0
- package/gallery/Waterfall.ui +11 -0
- package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
- package/gallery/data/acceptance/coil-gate.json +153 -0
- package/gallery/data/acceptance/orbit-baseline.json +163 -0
- package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
- package/gallery/data/acceptance/shots/gate-light.png +0 -0
- package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
- package/gallery/data/activity.json +1 -0
- package/gallery/data/bb-tasks-orb.json +420 -0
- package/gallery/data/biome.json +229 -0
- package/gallery/data/compare/status-after.png +0 -0
- package/gallery/data/compare/status-before.png +0 -0
- package/gallery/data/compare/status-dark-2x.png +0 -0
- package/gallery/data/compare/status-rerender.png +0 -0
- package/gallery/data/coverage-py.json +1 -0
- package/gallery/data/coverage-summary.json +1 -0
- package/gallery/data/coverage.out +23 -0
- package/gallery/data/deploys.json +1 -0
- package/gallery/data/device-runs.json +285 -0
- package/gallery/data/du-node-modules.txt +359 -0
- package/gallery/data/esbuild-meta.json +292 -0
- package/gallery/data/eslint.json +266 -0
- package/gallery/data/eslint.sarif +343 -0
- package/gallery/data/findings-review.json +76 -0
- package/gallery/data/gh-issues-nextjs.json +293 -0
- package/gallery/data/gh-pr-checks.json +272 -0
- package/gallery/data/gh-prs-nextjs.json +419 -0
- package/gallery/data/gh-run-failed.json +1 -0
- package/gallery/data/gh-run-passed.json +1 -0
- package/gallery/data/gh-run-running.json +1 -0
- package/gallery/data/github-release.json +57 -0
- package/gallery/data/hyperfine-startup.json +187 -0
- package/gallery/data/lcov.info +234 -0
- package/gallery/data/logs/media-sync.log +127 -0
- package/gallery/data/logs/thumbnailer.jsonl +17 -0
- package/gallery/data/logs/vitest-run.log +89 -0
- package/gallery/data/npm-audit.json +672 -0
- package/gallery/data/npm-outdated.json +179 -0
- package/gallery/data/oxlint.json +156 -0
- package/gallery/data/pip-outdated.json +68 -0
- package/gallery/data/pnpm-outdated.json +58 -0
- package/gallery/data/provenance/devicectl-install.json +35 -0
- package/gallery/data/provenance/install-timeout.log +4 -0
- package/gallery/data/provenance/iphone-install.json +72 -0
- package/gallery/data/provenance/iphone-no-receipt.json +57 -0
- package/gallery/data/provenance/live-home.jpg +0 -0
- package/gallery/data/provenance/live-probe.txt +10 -0
- package/gallery/data/provenance/xcodebuild-2811.log +19 -0
- package/gallery/data/provenance/xcodebuild-4712.log +19 -0
- package/gallery/data/pytest-junit.xml +64 -0
- package/gallery/data/python-org-trace.json +35 -0
- package/gallery/data/python-org.har +31 -0
- package/gallery/data/retry.ts +13 -0
- package/gallery/data/ruff.json +261 -0
- package/gallery/data/src/durations.py +63 -0
- package/gallery/data/src/json-logic.ts +156 -0
- package/gallery/data/src/mapMath.ts +135 -0
- package/gallery/data/src/semver/bump.go +73 -0
- package/gallery/data/traces/browser-error.json +5 -0
- package/gallery/data/traces/go/main.go +27 -0
- package/gallery/data/traces/go-panic.txt +16 -0
- package/gallery/data/traces/java-caused-by.txt +15 -0
- package/gallery/data/traces/node-aggregate.txt +30 -0
- package/gallery/data/traces/node-cause.txt +21 -0
- package/gallery/data/traces/py/deploy.json +5 -0
- package/gallery/data/traces/py/deploy.py +17 -0
- package/gallery/data/traces/py/settings.py +19 -0
- package/gallery/data/traces/python-chain.txt +30 -0
- package/gallery/data/traces/rust/main.rs +11 -0
- package/gallery/data/traces/rust-panic.txt +16 -0
- package/gallery/data/traces/shop/cart.mjs +19 -0
- package/gallery/data/traces/shop/pricing.mjs +12 -0
- package/gallery/data/traces/shop/status.mjs +9 -0
- package/gallery/data/traces/web/app.js +11 -0
- package/gallery/data/traces/web/index.html +1 -0
- package/gallery/data/traces/web/search.js +10 -0
- package/gallery/data/transcript/call-noisy.wav +0 -0
- package/gallery/data/transcript/call.json +362 -0
- package/gallery/data/transcript/call.wav +0 -0
- package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
- package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
- package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
- package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
- package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
- package/gallery/data/tsc.txt +4 -0
- package/gallery/data/vitest-bench.json +119 -0
- package/gallery/data/vitest-junit.xml +133 -0
- package/gallery/data/vitest-report.json +612 -0
- package/gallery/images/bell.svg +10 -0
- package/gallery/images/bike.svg +47 -0
- package/gallery/images/footer-planet.webp +0 -0
- package/gallery/images/front-light.svg +13 -0
- package/gallery/images/golden-hour.webp +0 -0
- package/gallery/images/option-flowers.webp +0 -0
- package/gallery/images/option-hilltop.webp +0 -0
- package/gallery/images/option-meadow.webp +0 -0
- package/gallery/images/option-sky.webp +0 -0
- package/gallery/images/u-lock.svg +10 -0
- package/index.client.tsx +164 -0
- package/index.server.ts +216 -0
- package/package.json +50 -4
- package/paseo-plugin.json +14 -0
- package/server/data-source.ts +407 -0
- package/server/project.ts +981 -0
- package/server/web/address.ts +57 -0
- package/server/web/cache.ts +85 -0
- package/server/web/commons.ts +126 -0
- package/server/web/geocode.ts +40 -0
- package/server/web/image.ts +35 -0
- package/server/web/lookups.ts +248 -0
- package/server/web/net.ts +210 -0
- package/shared/core/acceptance.ts +744 -0
- package/shared/core/ast.ts +76 -0
- package/shared/core/benchmark.ts +743 -0
- package/shared/core/catalog.ts +733 -0
- package/shared/core/compare.ts +225 -0
- package/shared/core/coverage.ts +456 -0
- package/shared/core/data.ts +334 -0
- package/shared/core/decisions.ts +527 -0
- package/shared/core/dependencies.ts +604 -0
- package/shared/core/diagnostics.ts +24 -0
- package/shared/core/environments.ts +212 -0
- package/shared/core/evaluate.ts +737 -0
- package/shared/core/fields.ts +66 -0
- package/shared/core/findings.ts +1172 -0
- package/shared/core/form.ts +69 -0
- package/shared/core/index.ts +44 -0
- package/shared/core/junit.ts +252 -0
- package/shared/core/lexer.ts +275 -0
- package/shared/core/logs.ts +496 -0
- package/shared/core/markdown.ts +2374 -0
- package/shared/core/parser.ts +355 -0
- package/shared/core/perf-units.ts +228 -0
- package/shared/core/pipeline.ts +679 -0
- package/shared/core/project.ts +472 -0
- package/shared/core/provenance.ts +725 -0
- package/shared/core/stack-trace.ts +718 -0
- package/shared/core/streaming.ts +136 -0
- package/shared/core/test-results.ts +562 -0
- package/shared/core/thread-activity.ts +203 -0
- package/shared/core/tracking.ts +887 -0
- package/shared/core/transcript.ts +1364 -0
- package/shared/core/treemap.ts +446 -0
- package/shared/core/types.ts +70 -0
- package/shared/core/validate.ts +574 -0
- package/shared/core/waterfall.ts +536 -0
- package/shared/diagram/flowchart.ts +205 -0
- package/shared/diagram/layout.ts +440 -0
- package/shared/diagram/markdown.ts +135 -0
- package/shared/diagram/sequence.ts +122 -0
- package/shared/diagram/zoom.ts +114 -0
- package/shared/fallback.ts +120 -0
- package/shared/genui.test.ts +122 -0
- package/shared/instructions.ts +144 -0
- package/shared/logic/calendarMath.ts +130 -0
- package/shared/logic/chartMath.ts +289 -0
- package/shared/logic/choose-logic.ts +196 -0
- package/shared/logic/compare-logic.ts +270 -0
- package/shared/logic/content-model.ts +227 -0
- package/shared/logic/environments-logic.ts +53 -0
- package/shared/logic/explain-logic.ts +315 -0
- package/shared/logic/fieldLogic.ts +138 -0
- package/shared/logic/gallery-search.ts +71 -0
- package/shared/logic/icons.ts +322 -0
- package/shared/logic/json-logic.ts +153 -0
- package/shared/logic/mapMath.ts +133 -0
- package/shared/logic/project-logic.ts +39 -0
- package/shared/logic/references-model.ts +201 -0
- package/shared/logic/sketch.ts +165 -0
- package/shared/logic/supersession.ts +77 -0
- package/shared/logic/values.ts +133 -0
- package/shared/logic/widget-logic.ts +163 -0
- package/shared/rpc.ts +252 -0
- package/shared/segment.ts +81 -0
- package/shared/settings.ts +12 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// What `Logs` shows: a command's output or a log file (text with ANSI colours,
|
|
2
|
+
// a list of lines, or JSON log records from pino, bunyan, winston, structlog,
|
|
3
|
+
// zap…) as numbered lines with a level each, quiet timestamps and fields,
|
|
4
|
+
// and the `file:line` locations they name. Also the pieces the viewer works
|
|
5
|
+
// with: repeats folded, search matches, and a line cut into styled runs.
|
|
6
|
+
// Pure: no React, no DOM.
|
|
7
|
+
import { isPlainObject } from "./evaluate";
|
|
8
|
+
import { ansiLines, CODE_EXTENSIONS, type AnsiColor, type AnsiSegment } from "./test-results";
|
|
9
|
+
import type { Value } from "./types";
|
|
10
|
+
|
|
11
|
+
export type LogLevel = "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
|
12
|
+
|
|
13
|
+
/** Most severe first. */
|
|
14
|
+
export const LOG_LEVELS: readonly LogLevel[] = ["fatal", "error", "warn", "info", "debug", "trace"];
|
|
15
|
+
|
|
16
|
+
export interface LogLink {
|
|
17
|
+
/** Offsets in the line's text. */
|
|
18
|
+
readonly start: number;
|
|
19
|
+
readonly end: number;
|
|
20
|
+
/** As written, `file://` dropped. */
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly line: number;
|
|
23
|
+
readonly column: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LogLine {
|
|
27
|
+
/** 1-based, in the order shown. */
|
|
28
|
+
readonly number: number;
|
|
29
|
+
/** Plain text: ANSI codes and control characters dropped. */
|
|
30
|
+
readonly text: string;
|
|
31
|
+
/** Colour runs whose texts join to `text`; null when the output had no colour codes. */
|
|
32
|
+
readonly segments: readonly AnsiSegment[] | null;
|
|
33
|
+
/** Detected on the line, or the level of the entry it continues. */
|
|
34
|
+
readonly level: LogLevel | null;
|
|
35
|
+
/** Part of the entry above: a stack line under a log line or a JSON record, a wrapped message. */
|
|
36
|
+
readonly continued: boolean;
|
|
37
|
+
/** Ranges of `text` shown quietly: the timestamp, a record's fields. */
|
|
38
|
+
readonly quiet: readonly (readonly [number, number])[];
|
|
39
|
+
/** Length of the leading timestamp (0 for none); repeats compare what follows. */
|
|
40
|
+
readonly stamp: number;
|
|
41
|
+
readonly links: readonly LogLink[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LogOutput {
|
|
45
|
+
readonly lines: readonly LogLine[];
|
|
46
|
+
readonly format: "text" | "records";
|
|
47
|
+
/** Entries per level (continuation lines not counted). */
|
|
48
|
+
readonly counts: Readonly<Record<LogLevel, number>>;
|
|
49
|
+
/** Fields every record has with the same value (`hostname`, `pid`, `service`): said once, not per line. */
|
|
50
|
+
readonly context: readonly (readonly [string, string])[];
|
|
51
|
+
/** Lines left out at the start of very long output. */
|
|
52
|
+
readonly dropped: number;
|
|
53
|
+
/** Record times are shown in UTC (records only). */
|
|
54
|
+
readonly utc: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type LogResult = { readonly ok: true; readonly output: LogOutput } | { readonly ok: false; readonly error: string };
|
|
58
|
+
|
|
59
|
+
/** Lines kept from very long output: the last ones, where a failure usually is. */
|
|
60
|
+
export const MAX_LOG_LINES = 20_000;
|
|
61
|
+
|
|
62
|
+
const SHAPE_HINT = "Logs needs text, a list of lines, or a list of JSON log records";
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Levels
|
|
66
|
+
|
|
67
|
+
const LEVEL_WORDS: Readonly<Record<string, LogLevel>> = {
|
|
68
|
+
fatal: "fatal",
|
|
69
|
+
panic: "fatal",
|
|
70
|
+
crit: "fatal",
|
|
71
|
+
critical: "fatal",
|
|
72
|
+
emerg: "fatal",
|
|
73
|
+
emergency: "fatal",
|
|
74
|
+
alert: "fatal",
|
|
75
|
+
f: "fatal",
|
|
76
|
+
a: "fatal",
|
|
77
|
+
error: "error",
|
|
78
|
+
err: "error",
|
|
79
|
+
e: "error",
|
|
80
|
+
severe: "error",
|
|
81
|
+
warn: "warn",
|
|
82
|
+
warning: "warn",
|
|
83
|
+
w: "warn",
|
|
84
|
+
info: "info",
|
|
85
|
+
information: "info",
|
|
86
|
+
informational: "info",
|
|
87
|
+
notice: "info",
|
|
88
|
+
i: "info",
|
|
89
|
+
debug: "debug",
|
|
90
|
+
dbg: "debug",
|
|
91
|
+
d: "debug",
|
|
92
|
+
verbose: "debug",
|
|
93
|
+
v: "debug",
|
|
94
|
+
fine: "debug",
|
|
95
|
+
config: "debug",
|
|
96
|
+
trace: "trace",
|
|
97
|
+
trc: "trace",
|
|
98
|
+
finer: "trace",
|
|
99
|
+
finest: "trace",
|
|
100
|
+
silly: "trace",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** A level as a record or logfmt pair writes it: a name, or a number (pino/bunyan 10–60; Python's `levelno` 10–50; syslog 0–7). */
|
|
104
|
+
export function levelOf(value: Value | undefined, scale: "pino" | "python" = "pino"): LogLevel | null {
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
const word = value.trim().toLowerCase();
|
|
107
|
+
if (/^\d+$/.test(word)) return levelOf(Number(word), scale);
|
|
108
|
+
return Object.hasOwn(LEVEL_WORDS, word) ? LEVEL_WORDS[word] : null;
|
|
109
|
+
}
|
|
110
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
111
|
+
if (scale === "python") return value >= 50 ? "fatal" : value >= 40 ? "error" : value >= 30 ? "warn" : value >= 20 ? "info" : value >= 10 ? "debug" : "trace";
|
|
112
|
+
if (value >= 0 && value <= 7 && Number.isInteger(value)) return value <= 2 ? "fatal" : value === 3 ? "error" : value === 4 ? "warn" : value <= 6 ? "info" : "debug";
|
|
113
|
+
return value >= 60 ? "fatal" : value >= 50 ? "error" : value >= 40 ? "warn" : value >= 30 ? "info" : value >= 20 ? "debug" : "trace";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const LEVEL_TOKEN = "fatal|panic|crit(?:ical)?|emerg(?:ency)?|alert|err(?:or)?|severe|warn(?:ing)?|info(?:rmation)?|notice|debug|dbg|verbose|trace|trc";
|
|
117
|
+
const JSON_LEVEL = /"(?:level|severity|lvl|levelname|log\.level)"\s*:\s*"?([\w]+)"?/i;
|
|
118
|
+
const LOGFMT_LEVEL = /(?:^|\s)(?:level|lvl|severity|loglevel)=("?)(\w+)\1(?=\s|$)/i;
|
|
119
|
+
const BRACKET_LEVEL = new RegExp(`[[<(]\\s*(${LEVEL_TOKEN}|[EWIDVF])\\s*[\\]>)]`, "i");
|
|
120
|
+
const UPPER_LEVEL = /(?:^|[\s|:[(])(FATAL|PANIC|CRITICAL|CRIT|EMERG|ALERT|SEVERE|ERROR|ERR|WARNING|WARN|NOTICE|INFO|DEBUG|DBG|TRACE|VERBOSE|FINEST|FINER|FINE)(?=$|[\s|:\]),-])/;
|
|
121
|
+
const COMPILER_LEVEL = /(?:^|[\s:)])(fatal error|error|warning)(?:\[[\w-]+\]| TS\d+| [A-Z]{2,}\d+)?:\s/;
|
|
122
|
+
const LOGCAT_LEVEL = /^(?:\d+\s+\d+\s+)?([VDIWEFA])[/ ][^\s:]*(?:\s*\(\s*\d+\))?:/;
|
|
123
|
+
const KLOG_LEVEL = /^([IWEF])\d{4} \d{2}:\d{2}:\d{2}\.\d+/;
|
|
124
|
+
const ERROR_START = /^(?:Uncaught\s+)?(?:[\w$.]*(?:Error|Exception)(?:\s\[[\w-]+\])?(?::\s|$)|Traceback \(most recent call last\):|panic: |FAIL\b|FAILED\b|npm (?:ERR!|error)\s|\s*[×✗✕✘❌]\s)/;
|
|
125
|
+
const WARN_START = /^(?:npm (?:WARN|warn)\s|\s*⚠\s|(?:\(node:\d+\) )?\[?[\w]*Warning\]?:\s)/;
|
|
126
|
+
|
|
127
|
+
/** The level a line of text names, read after its timestamp; null when it names none. */
|
|
128
|
+
export function textLevel(text: string): LogLevel | null {
|
|
129
|
+
const json = text.trimStart().startsWith("{") ? JSON_LEVEL.exec(text) : null;
|
|
130
|
+
if (json !== null) return levelOf(json[1]);
|
|
131
|
+
const logfmt = LOGFMT_LEVEL.exec(text);
|
|
132
|
+
if (logfmt !== null) return levelOf(logfmt[2]);
|
|
133
|
+
const syslog = /^<(\d{1,3})>/.exec(text);
|
|
134
|
+
if (syslog !== null) return levelOf(Number(syslog[1]) % 8);
|
|
135
|
+
const klog = KLOG_LEVEL.exec(text);
|
|
136
|
+
if (klog !== null) return LEVEL_WORDS[klog[1].toLowerCase()];
|
|
137
|
+
const logcat = LOGCAT_LEVEL.exec(text);
|
|
138
|
+
if (logcat !== null) return LEVEL_WORDS[logcat[1].toLowerCase()];
|
|
139
|
+
if (ERROR_START.test(text)) return "error";
|
|
140
|
+
if (WARN_START.test(text)) return "warn";
|
|
141
|
+
const head = text.slice(0, 64);
|
|
142
|
+
const bracket = BRACKET_LEVEL.exec(head);
|
|
143
|
+
// A single-letter bracket (`[E]`) only in capitals.
|
|
144
|
+
if (bracket !== null && (bracket[1].length > 1 || /[A-Z]/.test(bracket[1]))) return levelOf(bracket[1]);
|
|
145
|
+
const upper = UPPER_LEVEL.exec(head);
|
|
146
|
+
if (upper !== null) return levelOf(upper[1]);
|
|
147
|
+
const compiler = COMPILER_LEVEL.exec(text);
|
|
148
|
+
if (compiler !== null) return compiler[1] === "warning" ? "warn" : "error";
|
|
149
|
+
const leading = /^(Error|Fatal|Panic|Warning|Warn|Info|Debug|Trace)\b:?\s/.exec(text);
|
|
150
|
+
if (leading !== null) return levelOf(leading[1]);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Timestamps and links
|
|
156
|
+
|
|
157
|
+
const STAMP =
|
|
158
|
+
/^\[?(?:\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:[.,]\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2}|\sUTC)?|\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}(?:[.,]\d{1,9})?|[A-Z][a-z]{2} [ \d]\d \d{2}:\d{2}:\d{2}(?:\.\d+)?|\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}|\d{2}:\d{2}:\d{2}(?:[.,]\d{1,9})?)\]?(?=\s|$)/;
|
|
159
|
+
|
|
160
|
+
/** Length of the timestamp `text` starts with (ISO, syslog, logcat, clock times), 0 for none. */
|
|
161
|
+
export function stampLength(text: string): number {
|
|
162
|
+
return STAMP.exec(text)?.[0].length ?? 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const LINKS = new RegExp(
|
|
166
|
+
[
|
|
167
|
+
// Python `File "path", line 12`
|
|
168
|
+
`File "([^"]+\\.(?:${CODE_EXTENSIONS}))", line (\\d+)()`,
|
|
169
|
+
// TypeScript and MSBuild `path(12,5)`
|
|
170
|
+
`(?<![\\w./@-])((?:[A-Za-z]:)?[\\w./@+-]*[\\w@+-]\\.(?:${CODE_EXTENSIONS}))\\((\\d+),(\\d+)\\)`,
|
|
171
|
+
// `path:12`, `path:12:5`, `file:///abs/path:12:5`
|
|
172
|
+
`(?<![\\w.:/@-])(?:file:\\/\\/)?((?:[A-Za-z]:)?[\\w./@+-]*[\\w@+-]\\.(?:${CODE_EXTENSIONS})):(\\d+)(?::(\\d+))?(?![\\w.])`,
|
|
173
|
+
].join("|"),
|
|
174
|
+
"g",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/** The `file:line` locations a line names. */
|
|
178
|
+
export function findLinks(text: string): LogLink[] {
|
|
179
|
+
const links: LogLink[] = [];
|
|
180
|
+
for (const match of text.matchAll(LINKS)) {
|
|
181
|
+
const group = match[1] !== undefined ? 1 : match[4] !== undefined ? 4 : 7;
|
|
182
|
+
const line = Number(match[group + 1]);
|
|
183
|
+
if (!Number.isSafeInteger(line) || line < 1) continue;
|
|
184
|
+
const column = match[group + 2] ? Number(match[group + 2]) : null;
|
|
185
|
+
links.push({ start: match.index, end: match.index + match[0].length, path: match[group], line, column });
|
|
186
|
+
}
|
|
187
|
+
return links;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Reading output
|
|
192
|
+
|
|
193
|
+
type Fields = Readonly<Record<string, Value>>;
|
|
194
|
+
|
|
195
|
+
const CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g;
|
|
196
|
+
|
|
197
|
+
function emptyCounts(): Record<LogLevel, number> {
|
|
198
|
+
return { fatal: 0, error: 0, warn: 0, info: 0, debug: 0, trace: 0 };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The log `value` holds: text (one string), a list of lines, or a list of JSON records. */
|
|
202
|
+
export function readLogs(value: Value | undefined): LogResult {
|
|
203
|
+
if (typeof value === "string") return { ok: true, output: fromText(value) };
|
|
204
|
+
if (!Array.isArray(value)) {
|
|
205
|
+
if (value !== undefined && value !== null && isPlainObject(value)) {
|
|
206
|
+
// A report that holds its records: `{logs: [...]}`, `{entries: [...]}`, `{records: [...]}`.
|
|
207
|
+
for (const key of ["logs", "entries", "records", "lines", "events"]) {
|
|
208
|
+
const inner = (value as Fields)[key];
|
|
209
|
+
if (Array.isArray(inner)) return readLogs(inner);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { ok: false, error: SHAPE_HINT };
|
|
213
|
+
}
|
|
214
|
+
const items = value as readonly Value[];
|
|
215
|
+
if (items.length === 0) return { ok: true, output: fromText("") };
|
|
216
|
+
const records = items.filter((item) => isPlainObject(item) && isLogRecord(item as Fields)).length;
|
|
217
|
+
if (records > 0 && records >= items.length / 2) return { ok: true, output: fromRecords(items) };
|
|
218
|
+
const lines = items.map((item) => (typeof item === "string" ? item : typeof item === "number" || typeof item === "boolean" ? String(item) : JSON.stringify(item)));
|
|
219
|
+
return { ok: true, output: fromText(lines.join("\n")) };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const MESSAGE_KEYS = ["msg", "message", "event", "@message", "text", "log", "MESSAGE"];
|
|
223
|
+
const LEVEL_KEYS = ["level", "severity", "lvl", "levelname", "level_name", "loglevel", "log.level", "@level", "PRIORITY"];
|
|
224
|
+
const TIME_KEYS = ["time", "timestamp", "ts", "@timestamp", "t", "datetime", "asctime", "date", "__REALTIME_TIMESTAMP"];
|
|
225
|
+
const ERROR_KEYS = ["err", "error", "exception", "exc_info", "stack", "stack_trace", "stacktrace"];
|
|
226
|
+
|
|
227
|
+
function isLogRecord(item: Fields): boolean {
|
|
228
|
+
return [...MESSAGE_KEYS, ...LEVEL_KEYS, "levelno"].some((key) => Object.hasOwn(item, key));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function first(record: Fields, keys: readonly string[]): [string, Value] | null {
|
|
232
|
+
for (const key of keys) if (Object.hasOwn(record, key) && record[key] !== null && record[key] !== "") return [key, record[key]];
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A record's time as epoch ms: numbers in s or ms, ISO and other parseable strings. */
|
|
237
|
+
function recordTime(value: Value): number | null {
|
|
238
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
239
|
+
// Seconds (zap, structlog) up to the year 5138; journald's microseconds past 1e15.
|
|
240
|
+
return value > 1e15 ? value / 1000 : value < 1e11 ? value * 1000 : value;
|
|
241
|
+
}
|
|
242
|
+
if (typeof value === "string" && /\d{4}-\d{2}-\d{2}|\d{2}:\d{2}/.test(value)) {
|
|
243
|
+
const parsed = Date.parse(/^\d{4}-\d{2}-\d{2} \d/.test(value) ? value.replace(" ", "T").replace(",", ".") : value);
|
|
244
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function valueText(value: Value): string {
|
|
250
|
+
if (typeof value === "string") return /[\s"=]/.test(value) || value === "" ? JSON.stringify(value) : value;
|
|
251
|
+
const json = JSON.stringify(value) ?? "null";
|
|
252
|
+
return json.length > 160 ? `${json.slice(0, 159)}…` : json;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function fromRecords(items: readonly Value[]): LogOutput {
|
|
256
|
+
const records = items.map((item) => (isPlainObject(item) ? (item as Fields) : null));
|
|
257
|
+
const objects = records.filter((record): record is Fields => record !== null);
|
|
258
|
+
// Fields every record has with one value are context, said once.
|
|
259
|
+
const context: [string, string][] = [];
|
|
260
|
+
if (objects.length >= 2) {
|
|
261
|
+
const skip = new Set([...MESSAGE_KEYS, ...LEVEL_KEYS, ...TIME_KEYS, ...ERROR_KEYS, "levelno"]);
|
|
262
|
+
for (const [key, value] of Object.entries(objects[0])) {
|
|
263
|
+
if (skip.has(key) || (typeof value === "object" && value !== null)) continue;
|
|
264
|
+
if (objects.every((record) => Object.hasOwn(record, key) && record[key] === value)) context.push([key, valueText(value)]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const shared = new Set(context.map(([key]) => key));
|
|
268
|
+
const times = objects.map((record) => {
|
|
269
|
+
const found = first(record, TIME_KEYS);
|
|
270
|
+
return found === null ? null : recordTime(found[1]);
|
|
271
|
+
});
|
|
272
|
+
const known = times.filter((time): time is number => time !== null);
|
|
273
|
+
const oneDay = known.length > 0 && known.every((time) => new Date(time).toISOString().slice(0, 10) === new Date(known[0]).toISOString().slice(0, 10));
|
|
274
|
+
const lines: LogLine[] = [];
|
|
275
|
+
const counts = emptyCounts();
|
|
276
|
+
let objectIndex = 0;
|
|
277
|
+
for (const record of records) {
|
|
278
|
+
if (record === null) continue;
|
|
279
|
+
const time = times[objectIndex++];
|
|
280
|
+
const levelField = first(record, LEVEL_KEYS);
|
|
281
|
+
const level = levelField !== null ? levelOf(levelField[1]) : Object.hasOwn(record, "levelno") ? levelOf(record.levelno, "python") : null;
|
|
282
|
+
if (level !== null) counts[level]++;
|
|
283
|
+
const message = first(record, MESSAGE_KEYS);
|
|
284
|
+
const used = new Set([levelField?.[0], message?.[0], first(record, TIME_KEYS)?.[0], "levelno"]);
|
|
285
|
+
const continuation: string[] = [];
|
|
286
|
+
const fields: string[] = [];
|
|
287
|
+
for (const [key, value] of Object.entries(record)) {
|
|
288
|
+
if (used.has(key) || shared.has(key)) continue;
|
|
289
|
+
if (ERROR_KEYS.includes(key)) {
|
|
290
|
+
const stack = typeof value === "string" ? value : isPlainObject(value) && typeof (value as Fields).stack === "string" ? ((value as Fields).stack as string) : null;
|
|
291
|
+
if (stack !== null && stack.includes("\n")) {
|
|
292
|
+
continuation.push(...stack.replace(/\s+$/, "").split("\n"));
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
fields.push(`${key}=${valueText(value)}`);
|
|
297
|
+
}
|
|
298
|
+
const stamp = time === null ? "" : formatStamp(time, oneDay);
|
|
299
|
+
const body = message === null ? "" : typeof message[1] === "string" ? message[1] : JSON.stringify(message[1]);
|
|
300
|
+
const [head, ...wrapped] = body.replace(/\s+$/, "").split("\n");
|
|
301
|
+
const prefix = stamp === "" ? "" : `${stamp} `;
|
|
302
|
+
const tail = fields.length === 0 ? "" : `${head === "" ? "" : " "}${fields.join(" ")}`;
|
|
303
|
+
const text = `${prefix}${head}${tail}`.replace(CONTROL, "");
|
|
304
|
+
const quiet: [number, number][] = [];
|
|
305
|
+
if (stamp !== "") quiet.push([0, stamp.length]);
|
|
306
|
+
if (tail !== "") quiet.push([text.length - tail.length, text.length]);
|
|
307
|
+
lines.push(makeLine(lines.length + 1, text, null, level, false, quiet, stamp.length));
|
|
308
|
+
for (const extra of [...wrapped, ...continuation]) {
|
|
309
|
+
const plain = extra.replace(CONTROL, "");
|
|
310
|
+
lines.push(makeLine(lines.length + 1, plain, null, level, true, [], 0));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { lines, format: "records", counts, context, dropped: 0, utc: known.length > 0 };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function pad(n: number, width = 2): string {
|
|
317
|
+
return String(n).padStart(width, "0");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** UTC `HH:MM:SS.mmm`, with the date in front when the log spans days. */
|
|
321
|
+
function formatStamp(ms: number, oneDay: boolean): string {
|
|
322
|
+
const date = new Date(ms);
|
|
323
|
+
const clock = `${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}.${pad(date.getUTCMilliseconds(), 3)}`;
|
|
324
|
+
return oneDay ? clock : `${date.toISOString().slice(0, 10)} ${clock}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function makeLine(
|
|
328
|
+
number: number,
|
|
329
|
+
text: string,
|
|
330
|
+
segments: readonly AnsiSegment[] | null,
|
|
331
|
+
level: LogLevel | null,
|
|
332
|
+
continued: boolean,
|
|
333
|
+
quiet: readonly (readonly [number, number])[],
|
|
334
|
+
stamp: number,
|
|
335
|
+
): LogLine {
|
|
336
|
+
return { number, text, segments, level, continued, quiet, stamp, links: findLinks(text) };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** What a terminal shows of a line a progress bar redrew with `\r`: the last draw. */
|
|
340
|
+
function lastDraw(line: string): string {
|
|
341
|
+
const trimmed = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
342
|
+
const at = trimmed.lastIndexOf("\r");
|
|
343
|
+
return at === -1 ? trimmed : trimmed.slice(at + 1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Text whose lines are mostly JSON objects with a level or message is a JSONL log. */
|
|
347
|
+
function jsonLines(lines: readonly string[]): Value[] | null {
|
|
348
|
+
const filled = lines.filter((line) => line.trim() !== "");
|
|
349
|
+
if (filled.length < 2 || !filled.every((line, index) => index >= 20 || line.trimStart().startsWith("{"))) return null;
|
|
350
|
+
const parsed: Value[] = [];
|
|
351
|
+
for (const line of filled) {
|
|
352
|
+
try {
|
|
353
|
+
parsed.push(JSON.parse(line) as Value);
|
|
354
|
+
} catch {
|
|
355
|
+
parsed.push(line);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const records = parsed.filter((item) => isPlainObject(item) && isLogRecord(item as Fields)).length;
|
|
359
|
+
return records >= filled.length * 0.8 ? parsed : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function fromText(input: string): LogOutput {
|
|
363
|
+
const all = input.replace(/\r\n/g, "\n").split("\n").map(lastDraw);
|
|
364
|
+
while (all.length > 0 && all[all.length - 1].trim() === "") all.pop();
|
|
365
|
+
const blank = all.findIndex((line) => line.trim() !== "");
|
|
366
|
+
if (blank > 0) all.splice(0, blank);
|
|
367
|
+
const dropped = Math.max(0, all.length - MAX_LOG_LINES);
|
|
368
|
+
const kept = dropped > 0 ? all.slice(dropped) : all;
|
|
369
|
+
if (!input.includes("\x1b")) {
|
|
370
|
+
const records = jsonLines(kept);
|
|
371
|
+
if (records !== null) return { ...fromRecords(records), dropped };
|
|
372
|
+
}
|
|
373
|
+
const styled = input.includes("\x1b") || input.includes("\x9b") ? ansiLines(kept.join("\n")) : null;
|
|
374
|
+
const texts = styled === null ? kept.map((line) => line.replace(CONTROL, "")) : [];
|
|
375
|
+
const segments: (AnsiSegment[] | null)[] = [];
|
|
376
|
+
if (styled !== null) {
|
|
377
|
+
for (const line of styled) {
|
|
378
|
+
const clean = line.map((segment) => ({ ...segment, text: segment.text.replace(CONTROL, "") })).filter((segment) => segment.text !== "");
|
|
379
|
+
segments.push(clean);
|
|
380
|
+
texts.push(clean.map((segment) => segment.text).join(""));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const stamps = texts.map(stampLength);
|
|
384
|
+
const own = texts.map((text, index) => (text.trim() === "" ? null : textLevel(text.slice(stamps[index]).trimStart())));
|
|
385
|
+
const filled = texts.filter((text) => text.trim() !== "").length;
|
|
386
|
+
// A log is made of entries when most lines start with a timestamp, or most name a level: then a
|
|
387
|
+
// line without one (a stack line, a wrapped message, a blank line) continues the entry above.
|
|
388
|
+
const timestamped = filled > 0 && stamps.filter((stamp) => stamp > 0).length >= filled * 0.5;
|
|
389
|
+
const leveled = !timestamped && filled > 0 && own.filter((level) => level !== null).length >= filled * 0.5;
|
|
390
|
+
const counts = emptyCounts();
|
|
391
|
+
const lines: LogLine[] = [];
|
|
392
|
+
let entryLevel: LogLevel | null = null;
|
|
393
|
+
texts.forEach((text, index) => {
|
|
394
|
+
const stamp = stamps[index];
|
|
395
|
+
const continued = lines.length > 0 && (timestamped ? stamp === 0 : leveled && own[index] === null);
|
|
396
|
+
let level: LogLevel | null;
|
|
397
|
+
if (continued) level = entryLevel;
|
|
398
|
+
else {
|
|
399
|
+
level = own[index];
|
|
400
|
+
if (level !== null) counts[level]++;
|
|
401
|
+
entryLevel = level;
|
|
402
|
+
}
|
|
403
|
+
lines.push(makeLine(index + 1, text, styled === null ? null : segments[index], level, continued, stamp > 0 ? [[0, stamp]] : [], stamp));
|
|
404
|
+
});
|
|
405
|
+
return { lines, format: "text", counts, context: [], dropped, utc: false };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// The viewer's pieces
|
|
410
|
+
|
|
411
|
+
/** A shown row: a line, standing for `count` identical lines in a row. */
|
|
412
|
+
export interface LogRow {
|
|
413
|
+
readonly index: number;
|
|
414
|
+
readonly count: number;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Lines `indices` (in order) as rows, runs of identical lines (timestamps aside) folded into one. */
|
|
418
|
+
export function foldRepeats(lines: readonly LogLine[], indices: readonly number[]): LogRow[] {
|
|
419
|
+
const rows: LogRow[] = [];
|
|
420
|
+
let previous: string | null = null;
|
|
421
|
+
for (const index of indices) {
|
|
422
|
+
const line = lines[index];
|
|
423
|
+
const key = `${line.level}|${line.text.slice(line.stamp)}`;
|
|
424
|
+
const last = rows[rows.length - 1];
|
|
425
|
+
if (last !== undefined && key === previous && line.text.trim() !== "" && index === last.index + last.count) {
|
|
426
|
+
rows[rows.length - 1] = { index: last.index, count: last.count + 1 };
|
|
427
|
+
} else {
|
|
428
|
+
rows.push({ index, count: 1 });
|
|
429
|
+
}
|
|
430
|
+
previous = key;
|
|
431
|
+
}
|
|
432
|
+
return rows;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** Where `query` occurs in `text`, case-insensitively, as [start, end) pairs. */
|
|
436
|
+
export function occurrences(text: string, query: string): [number, number][] {
|
|
437
|
+
if (query === "") return [];
|
|
438
|
+
const haystack = text.toLowerCase();
|
|
439
|
+
const needle = query.toLowerCase();
|
|
440
|
+
const found: [number, number][] = [];
|
|
441
|
+
for (let at = haystack.indexOf(needle); at !== -1; at = haystack.indexOf(needle, at + needle.length)) found.push([at, at + needle.length]);
|
|
442
|
+
return found;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export interface LogRun {
|
|
446
|
+
readonly text: string;
|
|
447
|
+
readonly color: AnsiColor | null;
|
|
448
|
+
readonly bold: boolean;
|
|
449
|
+
readonly inverse: boolean;
|
|
450
|
+
readonly quiet: boolean;
|
|
451
|
+
readonly link: LogLink | null;
|
|
452
|
+
readonly match: boolean;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** A line cut into runs of one style each: its colours, quiet ranges, links and search matches. */
|
|
456
|
+
export function lineRuns(line: LogLine, query: string): LogRun[] {
|
|
457
|
+
const text = line.text;
|
|
458
|
+
if (text === "") return [];
|
|
459
|
+
const matches = occurrences(text, query);
|
|
460
|
+
const cuts = new Set<number>([0, text.length]);
|
|
461
|
+
let offset = 0;
|
|
462
|
+
const segments = line.segments ?? [{ text, color: null, bold: false, inverse: false }];
|
|
463
|
+
const segmentStarts: number[] = [];
|
|
464
|
+
for (const segment of segments) {
|
|
465
|
+
segmentStarts.push(offset);
|
|
466
|
+
cuts.add(offset);
|
|
467
|
+
offset += segment.text.length;
|
|
468
|
+
}
|
|
469
|
+
for (const [start, end] of [...line.quiet, ...matches]) cuts.add(start).add(end);
|
|
470
|
+
for (const link of line.links) cuts.add(link.start).add(link.end);
|
|
471
|
+
const points = [...cuts].filter((point) => point >= 0 && point <= text.length).sort((a, b) => a - b);
|
|
472
|
+
const runs: LogRun[] = [];
|
|
473
|
+
let segmentIndex = 0;
|
|
474
|
+
for (let at = 0; at < points.length - 1; at++) {
|
|
475
|
+
const start = points[at];
|
|
476
|
+
const end = points[at + 1];
|
|
477
|
+
if (end <= start) continue;
|
|
478
|
+
while (segmentIndex + 1 < segments.length && segmentStarts[segmentIndex + 1] <= start) segmentIndex++;
|
|
479
|
+
const segment = segments[segmentIndex];
|
|
480
|
+
runs.push({
|
|
481
|
+
text: text.slice(start, end),
|
|
482
|
+
color: segment.color,
|
|
483
|
+
bold: segment.bold,
|
|
484
|
+
inverse: segment.inverse,
|
|
485
|
+
quiet: line.quiet.some(([from, to]) => start >= from && end <= to),
|
|
486
|
+
link: line.links.find((link) => start >= link.start && end <= link.end) ?? null,
|
|
487
|
+
match: matches.some(([from, to]) => start >= from && end <= to),
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return runs;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** The first line at error or fatal level that starts an entry; -1 when there is none. */
|
|
494
|
+
export function firstError(lines: readonly LogLine[]): number {
|
|
495
|
+
return lines.findIndex((line) => (line.level === "error" || line.level === "fatal") && !line.continued);
|
|
496
|
+
}
|