diff --git a/tools/comment-preview/.gitignore b/tools/comment-preview/.gitignore new file mode 100644 index 0000000..89f9ac0 --- /dev/null +++ b/tools/comment-preview/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/tools/comment-preview/fixtures.ts b/tools/comment-preview/fixtures.ts new file mode 100644 index 0000000..f6d26ae --- /dev/null +++ b/tools/comment-preview/fixtures.ts @@ -0,0 +1,309 @@ +// Every shape the PR comment can take, as view models the template consumes. +// +// These are `buildViewModel` output, not `ReportContext` input — the point is to +// iterate on `success.md.j2` without a CI run, so the fixtures start where the +// template does. A change to `buildViewModel` itself will not show up here. + +import { formatCost } from "../../src/reporters/github/github.ts"; +import { schemaChangeLabel } from "../../src/reporters/github/schema-change.ts"; + +const ASSETS = "https://raw.githubusercontent.com/Query-Doctor/analyzer/main/assets"; + +/** Fields every fixture shares. Individual fixtures override what they exercise. */ +const base = { + brandMark: `${ASSETS}/brand/mark.svg`, + gateIconBase: `${ASSETS}/gate`, + formatCost, + schemaChangeLabel, + queryLinks: {}, + displayRecommendations: [], + displayRegressed: [], + tableGrowth: [], + schemaChange: { hasChanges: false, groups: [] }, + hasComparison: true, + comparisonBranch: "staging", + runUrl: "https://app.querydoctor.com/query-doctor/site/ci/019faf39", + queryStats: { analyzed: 103 }, + statisticsMode: { kind: "fromStatisticsExport" }, + runMetadata: { + footer: 'More detail → get_ci_run(runId: "019faf39")', + docsUrl: "https://docs.querydoctor.com", + }, +}; + +const gate = ( + condition: string, + label: string, + conclusion: "failure" | "neutral" | "success", + found: string, + fired: boolean, +) => ({ condition, label, conclusion, found, fired }); + +const PASSING = { + regression: gate("regression-beyond-threshold", "Cost regression", "success", "No query went up more than 10%", false), + untested: gate("untested-data-access", "Untested data access", "success", "No changed data-access file without a test", false), + newQuery: gate("new-query", "New query", "success", "No new queries", false), + newQueryIndex: gate("new-query-index", "New query with index recommendation", "success", "No new query ships an index recommendation", false), + schema: gate("schema-drift", "Schema drift", "success", "No schema changes", false), + nudge: gate("high-value-nudge", "High-value nudge", "success", "No index or rewrite past the threshold", false), +}; + +const allPassing = () => Object.values(PASSING); + +const site = (name: string, file: string) => ({ name, file }); + +const regressedQuery = ( + hash: string, + name: string, + file: string, + previousCost: number, + currentCost: number, + regressionPercentage: number, +) => ({ hash, queryPreview: "select …", site: site(name, file), previousCost, currentCost, regressionPercentage }); + +const recommendation = ( + fingerprint: string, + name: string, + file: string, + baseCost: number, + optimizedCost: number, + proposedIndexes: string[], +) => ({ fingerprint, queryPreview: "select …", site: site(name, file), baseCost, optimizedCost, proposedIndexes }); + +const entry = (kind: string, object: string, name: string, detail?: string) => ({ kind, object, name, detail }); + +const REGRESSED = [ + regressedQuery("a1", "ProjectQueriesRepository.search", "apps/api/src/projects/project-queries.repository.ts", 1245, 1876, 50.7), + regressedQuery("b2", "CiRepository.findRunsByProject", "apps/api/src/ci/ci.repository.ts", 880, 1190, 35.2), +]; + +const RECOMMENDATIONS = [ + recommendation("f1", "DashboardRepository.findFunnelCounts", "apps/api/src/dashboard/dashboard.repository.ts", 4820, 312, [ + "project_queries (project_id, last_seen_at)", + ]), + recommendation("f2", "StatisticsRepository.latestSnapshot", "apps/api/src/statistics/statistics.repository.ts", 2100, 640, [ + "statistics_snapshots (project_id, captured_at)", + "statistics_payloads (project_id)", + ]), +]; + +const SCHEMA_GROUPS = [ + { + kind: "added", + entries: [ + entry("added", "table", "public.project_table_statistics"), + entry("added", "table", "public.statistics_payloads"), + entry("added", "table", "public.statistics_snapshots"), + entry("added", "index", "statistics_payloads.statistics_payloads_project_id_hash_idx"), + entry("added", "constraint", "statistics_snapshots.statistics_snapshots_pkey"), + ], + }, + { kind: "changed", entries: [entry("changed", "index", "", "isUnique")] }, + { kind: "removed", entries: [entry("removed", "index", "")] }, +]; + +const GROWTH = [ + { table: "project_queries", before: 3503, after: 6290, percent: 80 }, + { table: "ci_runs_queries", before: 41200, after: 52800, percent: 28 }, +]; + +export interface Fixture { + name: string; + /** What a reader should be checking in this render. */ + note: string; + view: Record; +} + +export const fixtures: Fixture[] = [ + { + name: "clean run", + note: "The common case. Six lines, no detail, nothing to act on.", + view: { ...base, gateSummary: { failing: 0, successful: 6, neutral: 0 }, gates: allPassing() }, + }, + { + name: "cost regression, with table growth", + note: "The growth note is a caption on the gate row, so it sits tight beneath it.", + view: { + ...base, + gateSummary: { failing: 1, successful: 5, neutral: 0 }, + tableGrowth: GROWTH, + displayRegressed: REGRESSED, + gates: [ + gate("regression-beyond-threshold", "Cost regression", "failure", "2 queries went up more than 10%, where either cost is at least 100", true), + PASSING.untested, PASSING.newQuery, PASSING.newQueryIndex, PASSING.schema, PASSING.nudge, + ], + }, + }, + { + name: "cost regression, no table growth", + note: "Same gate with nothing to attribute the move to. The first query still starts where the growth note would.", + view: { + ...base, + gateSummary: { failing: 1, successful: 5, neutral: 0 }, + displayRegressed: REGRESSED, + gates: [ + gate("regression-beyond-threshold", "Cost regression", "failure", "2 queries went up more than 10%, where either cost is at least 100", true), + PASSING.untested, PASSING.newQuery, PASSING.newQueryIndex, PASSING.schema, PASSING.nudge, + ], + }, + }, + { + name: "new query with index recommendation", + note: "Each recommendation is a name, a caption, and the DDL that clears it.", + view: { + ...base, + gateSummary: { failing: 1, successful: 4, neutral: 1 }, + displayRecommendations: RECOMMENDATIONS, + gates: [ + gate("new-query-index", "New query with index recommendation", "failure", "2 new queries ship a high-impact index recommendation", true), + gate("new-query", "New query", "neutral", "6 new queries, none with a prior baseline", true), + PASSING.regression, PASSING.untested, PASSING.schema, PASSING.nudge, + ], + }, + }, + { + name: "schema drift", + note: "Added, Changed and Removed entries. Check the leading in and out of the block.", + view: { + ...base, + gateSummary: { failing: 1, successful: 5, neutral: 0 }, + schemaChange: { hasChanges: true, groups: SCHEMA_GROUPS }, + gates: [ + gate("schema-drift", "Schema drift", "failure", "The schema changed against the baseline", true), + PASSING.regression, PASSING.untested, PASSING.newQuery, PASSING.newQueryIndex, PASSING.nudge, + ], + }, + }, + { + name: "everything fires", + note: "The worst realistic case: three expansions stacked, roster split across them.", + view: { + ...base, + gateSummary: { failing: 3, successful: 2, neutral: 1 }, + tableGrowth: GROWTH, + displayRegressed: REGRESSED, + displayRecommendations: RECOMMENDATIONS, + schemaChange: { hasChanges: true, groups: SCHEMA_GROUPS }, + gates: [ + gate("regression-beyond-threshold", "Cost regression", "failure", "2 queries went up more than 10%, where either cost is at least 100", true), + gate("new-query-index", "New query with index recommendation", "failure", "2 new queries ship a high-impact index recommendation", true), + gate("schema-drift", "Schema drift", "failure", "The schema changed against the baseline", true), + gate("new-query", "New query", "neutral", "6 new queries, none with a prior baseline", true), + PASSING.untested, PASSING.nudge, + ], + }, + }, + { + name: "volume stress — 12 regressions", + note: "Where the expansion stops being scannable. Check whether the roster is still findable.", + view: { + ...base, + gateSummary: { failing: 1, successful: 5, neutral: 0 }, + tableGrowth: GROWTH, + displayRegressed: Array.from({ length: 12 }, (_, i) => + regressedQuery(`h${i}`, `Repository.method${i}`, `apps/api/src/module${i}/module${i}.repository.ts`, 900 + i * 40, 1400 + i * 90, 30 + i * 3), + ), + gates: [ + gate("regression-beyond-threshold", "Cost regression", "failure", "12 queries went up more than 10%, where either cost is at least 100", true), + PASSING.untested, PASSING.newQuery, PASSING.newQueryIndex, PASSING.schema, PASSING.nudge, + ], + }, + }, + { + name: "single of everything", + note: "Singular wording. Every count is 1.", + view: { + ...base, + gateSummary: { failing: 1, successful: 4, neutral: 1 }, + displayRegressed: [REGRESSED[0]], + schemaChange: { hasChanges: true, groups: [{ kind: "added", entries: [entry("added", "table", "public.statistics_snapshots")] }] }, + gates: [ + gate("regression-beyond-threshold", "Cost regression", "failure", "1 query went up more than 10%, where either cost is at least 100", true), + gate("new-query", "New query", "neutral", "1 new query, with no prior baseline", true), + PASSING.untested, PASSING.newQueryIndex, PASSING.schema, PASSING.nudge, + ], + }, + }, + { + name: "assumed statistics", + note: "No production stats loaded. The footer says so instead of claiming a measured run.", + view: { + ...base, + gateSummary: { failing: 0, successful: 6, neutral: 0 }, + statisticsMode: { kind: "fromAssumption", reltuples: 10000000 }, + gates: allPassing(), + }, + }, + { + name: "no baseline", + note: "Nothing to compare against. No gate roster at all.", + view: { ...base, gateSummary: null, hasComparison: false, comparisonUnavailable: false }, + }, + { + name: "comparison unavailable", + note: "Baseline exists but could not be read this run. Must read as temporary.", + view: { ...base, gateSummary: null, hasComparison: false, comparisonUnavailable: true }, + }, + { + name: "comparison branch unset", + note: "Warning callout above the roster.", + view: { + ...base, + gateSummary: { failing: 0, successful: 6, neutral: 0 }, + gates: allPassing(), + runMetadata: { + ...base.runMetadata, + baseline: { unset: true, resolvedBranch: "main", headVsHead: false, mcpCall: 'get_repo_config(repo: "query-doctor/site")' }, + }, + }, + }, + { + name: "ingest failed — auth", + note: "Nothing was saved. The reader has to know the numbers below are not on the dashboard.", + view: { + ...base, + gateSummary: { failing: 0, successful: 6, neutral: 0 }, + gates: allPassing(), + ingestError: { kind: "auth", status: 401 }, + }, + }, + { + name: "ingest failed — payload too large", + note: "Re-running will not help. Includes the details disclosure.", + view: { + ...base, + gateSummary: { failing: 0, successful: 6, neutral: 0 }, + gates: allPassing(), + ingestError: { kind: "too_large", status: 413, message: "payload exceeds 6 MB" }, + }, + }, + { + name: "untested data access", + note: "The test-presence callout with its file list.", + view: { + ...base, + gateSummary: { failing: 1, successful: 5, neutral: 0 }, + gates: [ + gate("untested-data-access", "Untested data access", "failure", "2 changed data-access files ship with no related test", true), + PASSING.regression, PASSING.newQuery, PASSING.newQueryIndex, PASSING.schema, PASSING.nudge, + ], + testPresenceVerdict: { + reason: "Two repository files changed and neither has a data-layer test beside it.", + nextStep: "Add a test that exercises the changed query against a real planner.", + dataAccessFiles: ["apps/api/src/projects/project-queries.repository.ts", "apps/api/src/ci/ci.repository.ts"], + triageHint: "Triage with triage_query if this is intentional.", + }, + testPresenceConclusion: "failure", + }, + }, + { + name: "modeled tables notice", + note: "Some tables have no loaded statistics, so their costs are modeled.", + view: { + ...base, + gateSummary: { failing: 0, successful: 6, neutral: 0 }, + gates: allPassing(), + modeledTablesNotice: { count: 3, list: "`audit_log`, `webhooks`, `sessions`" }, + }, + }, +]; diff --git a/tools/comment-preview/github-markdown-dark.css b/tools/comment-preview/github-markdown-dark.css new file mode 100644 index 0000000..f307e51 --- /dev/null +++ b/tools/comment-preview/github-markdown-dark.css @@ -0,0 +1,1124 @@ +/*dark */ +.markdown-body { + color-scheme: dark; + /** CSS default easing. Use for hover state changes and micro-interactions. */ + /** Accelerating motion. Use for elements exiting the viewport (moving off-screen). */ + /** Smooth acceleration and deceleration. Use for elements moving or morphing within the viewport. */ + /** Decelerating motion. Use for elements entering the viewport or appearing on screen. */ + /** Constant motion with no acceleration. Use for continuous animations like progress bars or loaders. */ + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + font-weight: 400; + color: #f0f6fc; + background-color: #0d1117; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body a { + text-decoration: underline; + text-underline-offset: .2rem; +} + +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); +} + +.markdown-body details, +.markdown-body figcaption, +.markdown-body figure { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body [hidden] { + display: none !important; +} + +.markdown-body a { + background-color: rgba(0,0,0,0); + color: #4493f8; + text-decoration: none; +} + +.markdown-body abbr[title] { + border-bottom: none; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +.markdown-body b, +.markdown-body strong { + font-weight: 600; +} + +.markdown-body dfn { + font-style: italic; +} + +.markdown-body h1 { + margin: .67em 0; + font-weight: 600; + padding-bottom: .3em; + font-size: 2em; + border-bottom: 1px solid #3d444db3; +} + +.markdown-body mark { + background-color: #bb800926; + color: #f0f6fc; +} + +.markdown-body small { + font-size: 90%; +} + +.markdown-body sub, +.markdown-body sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +.markdown-body sub { + bottom: -0.25em; +} + +.markdown-body sup { + top: -0.5em; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre, +.markdown-body samp { + font-family: monospace; + font-size: 1em; +} + +.markdown-body figure { + margin: 1em 2.5rem; +} + +.markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: rgba(0,0,0,0); + border-bottom: 1px solid #3d444db3; + height: .25em; + padding: 0; + margin: 1.5rem 0; + background-color: #3d444d; + border: 0; +} + +.markdown-body input { + font: inherit; + margin: 0; + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body [type=button], +.markdown-body [type=reset], +.markdown-body [type=submit] { + -webkit-appearance: button; + appearance: button; +} + +.markdown-body [type=checkbox], +.markdown-body [type=radio] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body [type=number]::-webkit-inner-spin-button, +.markdown-body [type=number]::-webkit-outer-spin-button { + height: auto; +} + +.markdown-body [type=search]::-webkit-search-cancel-button, +.markdown-body [type=search]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + +.markdown-body ::-webkit-input-placeholder { + color: inherit; + opacity: .54; +} + +.markdown-body ::-webkit-file-upload-button { + -webkit-appearance: button; + appearance: button; + font: inherit; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body ::placeholder { + color: #9198a1; + opacity: 1; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; + font-variant: tabular-nums; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body a:focus, +.markdown-body [role=button]:focus, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=checkbox]:focus { + outline: 2px solid var(--borderColor-accent-emphasis); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:focus:not(:focus-visible), +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body input[type=radio]:focus:not(:focus-visible), +.markdown-body input[type=checkbox]:focus:not(:focus-visible) { + outline: solid 1px rgba(0,0,0,0); +} + +.markdown-body a:focus-visible, +.markdown-body [role=button]:focus-visible, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus-visible { + outline: 2px solid var(--borderColor-accent-emphasis); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:not([class]):focus, +.markdown-body a:not([class]):focus-visible, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus, +.markdown-body input[type=checkbox]:focus-visible { + outline-offset: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: 0.25rem; + font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + line-height: 10px; + color: #f0f6fc; + vertical-align: middle; + background-color: #151b23; + border: solid 1px var(--borderColor-muted); + border-bottom-color: var(--borderColor-muted); + border-radius: 6px; + box-shadow: inset 0 -1px 0 var(--borderColor-muted); +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 1.5rem; + margin-bottom: 1rem; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h2 { + font-weight: 600; + padding-bottom: .3em; + font-size: 1.5em; + border-bottom: 1px solid #3d444db3; +} + +.markdown-body h3 { + font-weight: 600; + font-size: 1.25em; +} + +.markdown-body h4 { + font-weight: 600; + font-size: 1em; +} + +.markdown-body h5 { + font-weight: 600; + font-size: .875em; +} + +.markdown-body h6 { + font-weight: 600; + font-size: .85em; + color: #9198a1; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: #9198a1; + border-left: .25em solid #3d444d; +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body tt, +.markdown-body code, +.markdown-body samp { + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 12px; + word-wrap: normal; +} + +.markdown-body .octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; +} + +.markdown-body input::-webkit-outer-spin-button, +.markdown-body input::-webkit-inner-spin-button { + margin: 0; + appearance: none; +} + +.markdown-body .mr-2 { + margin-right: 0.5rem !important; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .absent { + color: #f85149; +} + +.markdown-body .anchor { + float: left; + padding-right: 0.25rem; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin-top: 0; + margin-bottom: 1rem; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #f0f6fc; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + padding: 0 .2em; + font-size: inherit; +} + +.markdown-body summary h1, +.markdown-body summary h2, +.markdown-body summary h3, +.markdown-body summary h4, +.markdown-body summary h5, +.markdown-body summary h6 { + display: inline-block; +} + +.markdown-body summary h1 .anchor, +.markdown-body summary h2 .anchor, +.markdown-body summary h3 .anchor, +.markdown-body summary h4 .anchor, +.markdown-body summary h5 .anchor, +.markdown-body summary h6 .anchor { + margin-left: -40px; +} + +.markdown-body summary h1, +.markdown-body summary h2 { + padding-bottom: 0; + border-bottom: 0; +} + +.markdown-body ul.no-list, +.markdown-body ol.no-list { + padding: 0; + list-style-type: none; +} + +.markdown-body ol[type="a s"] { + list-style-type: lower-alpha; +} + +.markdown-body ol[type="A s"] { + list-style-type: upper-alpha; +} + +.markdown-body ol[type="i s"] { + list-style-type: lower-roman; +} + +.markdown-body ol[type="I s"] { + list-style-type: upper-roman; +} + +.markdown-body ol[type="1"] { + list-style-type: decimal; +} + +.markdown-body div>ol:not([type]) { + list-style-type: decimal; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 1rem; +} + +.markdown-body li+li { + margin-top: .25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 1rem; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 1rem; + margin-bottom: 1rem; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #3d444d; +} + +.markdown-body table td>:last-child { + margin-bottom: 0; +} + +.markdown-body table tr { + background-color: #0d1117; + border-top: 1px solid #3d444db3; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #151b23; +} + +.markdown-body table img { + background-color: rgba(0,0,0,0); +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body .emoji { + max-width: none; + vertical-align: text-top; + background-color: rgba(0,0,0,0); +} + +.markdown-body span.frame { + display: block; + overflow: hidden; +} + +.markdown-body span.frame>span { + display: block; + float: left; + width: auto; + padding: 7px; + margin: 13px 0 0; + overflow: hidden; + border: 1px solid #3d444d; +} + +.markdown-body span.frame span img { + display: block; + float: left; +} + +.markdown-body span.frame span span { + display: block; + padding: 5px 0 0; + clear: both; + color: #f0f6fc; +} + +.markdown-body span.align-center { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-center>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} + +.markdown-body span.align-center span img { + margin: 0 auto; + text-align: center; +} + +.markdown-body span.align-right { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-right>span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} + +.markdown-body span.align-right span img { + margin: 0; + text-align: right; +} + +.markdown-body span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} + +.markdown-body span.float-left span { + margin: 13px 0 0; +} + +.markdown-body span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} + +.markdown-body span.float-right>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} + +.markdown-body code, +.markdown-body tt { + padding: .2em .4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: #656c7633; + border-radius: 6px; +} + +.markdown-body code br, +.markdown-body tt br { + display: none; +} + +.markdown-body del code { + text-decoration: inherit; +} + +.markdown-body samp { + font-size: 85%; +} + +.markdown-body pre code { + font-size: 100%; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + word-break: normal; + white-space: pre; + background: rgba(0,0,0,0); + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 1rem; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 1rem; + overflow: auto; + font-size: 85%; + line-height: 1.45; + color: #f0f6fc; + background-color: #151b23; + border-radius: 6px; +} + +.markdown-body pre code, +.markdown-body pre tt { + display: inline; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: rgba(0,0,0,0); + border: 0; +} + +.markdown-body .csv-data td, +.markdown-body .csv-data th { + padding: 5px; + overflow: hidden; + font-size: 12px; + line-height: 1; + text-align: left; + white-space: nowrap; +} + +.markdown-body .csv-data .blob-num { + padding: 10px 0.5rem 9px; + text-align: right; + background: #0d1117; + border: 0; +} + +.markdown-body .csv-data tr { + border-top: 0; +} + +.markdown-body .csv-data th { + font-weight: 600; + background: #151b23; + border-top: 0; +} + +.markdown-body [data-footnote-ref]::before { + content: "["; +} + +.markdown-body [data-footnote-ref]::after { + content: "]"; +} + +.markdown-body .footnotes { + font-size: 12px; + color: #9198a1; + border-top: 1px solid #3d444d; +} + +.markdown-body .footnotes ol { + padding-left: 1rem; +} + +.markdown-body .footnotes ol ul { + display: inline-block; + padding-left: 1rem; + margin-top: 1rem; +} + +.markdown-body .footnotes li { + position: relative; +} + +.markdown-body .footnotes li:target::before { + position: absolute; + top: calc(0.5rem*-1); + right: calc(0.5rem*-1); + bottom: calc(0.5rem*-1); + left: calc(1.5rem*-1); + pointer-events: none; + content: ""; + border: 2px solid #1f6feb; + border-radius: 6px; +} + +.markdown-body .footnotes li:target { + color: #f0f6fc; +} + +.markdown-body .footnotes .data-footnote-backref g-emoji { + font-family: monospace; +} + +.markdown-body .pl-c { + color: #9198a1; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #79c0ff; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #d2a8ff; +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: #f0f6fc; +} + +.markdown-body .pl-ent { + color: #7ee787; +} + +.markdown-body .pl-k { + color: #ff7b72; +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: #a5d6ff; +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: #ffa657; +} + +.markdown-body .pl-bu { + color: #f85149; +} + +.markdown-body .pl-ii { + color: var(--fgColor-danger); + background-color: var(--bgColor-danger-muted); +} + +.markdown-body .pl-c2 { + color: #f0f6fc; + background-color: #b62324; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: #7ee787; +} + +.markdown-body .pl-ml { + color: #f2cc60; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: #1f6feb; +} + +.markdown-body .pl-mi { + font-style: italic; + color: #f0f6fc; +} + +.markdown-body .pl-mb { + font-weight: bold; + color: #f0f6fc; +} + +.markdown-body .pl-md { + color: #ffdcd7; + background-color: #67060c; +} + +.markdown-body .pl-mi1 { + color: #aff5b4; + background-color: #033a16; +} + +.markdown-body .pl-mc { + color: #ffdfb6; + background-color: #5a1e02; +} + +.markdown-body .pl-mi2 { + color: #f0f6fc; + background-color: #1158c7; +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: #d2a8ff; +} + +.markdown-body .pl-ba { + color: #9198a1; +} + +.markdown-body .pl-sg { + color: #3d444d; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #a5d6ff; +} + +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible), +.markdown-body button:focus:not(:focus-visible), +.markdown-body summary:focus:not(:focus-visible), +.markdown-body a:focus:not(:focus-visible) { + outline: none; + box-shadow: none; +} + +.markdown-body [tabindex="0"]:focus:not(:focus-visible), +.markdown-body details-dialog:focus:not(:focus-visible) { + outline: none; +} + +.markdown-body g-emoji { + display: inline-block; + min-width: 1ch; + font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-size: 1em; + font-style: normal !important; + font-weight: 400; + line-height: 1; + vertical-align: -0.075em; +} + +.markdown-body g-emoji img { + width: 1em; + height: 1em; +} + +.markdown-body a:has(>p,>div,>pre,>blockquote) { + display: block; +} + +.markdown-body a:has(>p,>div,>pre,>blockquote):not(:has(.snippet-clipboard-content,>pre)) { + width: fit-content; +} + +.markdown-body a:has(>p,>div,>pre,>blockquote):has(.snippet-clipboard-content,>pre):focus-visible { + outline: 2px solid var(--borderColor-accent-emphasis); + outline-offset: 2px; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item label { + font-weight: 400; +} + +.markdown-body .task-list-item.enabled label { + cursor: pointer; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 0.25rem; +} + +.markdown-body .task-list-item .handle { + display: none; +} + +.markdown-body .task-list-item-checkbox { + margin: 0 .2em .25em -1.4em; + vertical-align: middle; +} + +.markdown-body ul:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body ol:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body .contains-task-list:hover .task-list-item-convert-container, +.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { + display: block; + width: auto; + height: 24px; + overflow: visible; + clip-path: none; +} + +.markdown-body ::-webkit-calendar-picker-indicator { + filter: invert(50%); +} + +.markdown-body .markdown-alert { + padding: 0.5rem 1rem; + margin-bottom: 1rem; + color: inherit; + border-left: .25em solid #3d444d; +} + +.markdown-body .markdown-alert>:first-child { + margin-top: 0; +} + +.markdown-body .markdown-alert>:last-child { + margin-bottom: 0; +} + +.markdown-body .markdown-alert .markdown-alert-title { + display: flex; + font-weight: 500; + align-items: center; + line-height: 1; +} + +.markdown-body .markdown-alert.markdown-alert-note { + border-left-color: #1f6feb; +} + +.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { + color: #4493f8; +} + +.markdown-body .markdown-alert.markdown-alert-important { + border-left-color: #8957e5; +} + +.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { + color: #ab7df8; +} + +.markdown-body .markdown-alert.markdown-alert-warning { + border-left-color: #9e6a03; +} + +.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { + color: #d29922; +} + +.markdown-body .markdown-alert.markdown-alert-tip { + border-left-color: #238636; +} + +.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { + color: #3fb950; +} + +.markdown-body .markdown-alert.markdown-alert-caution { + border-left-color: #da3633; +} + +.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { + color: #f85149; +} + +.markdown-body>*:first-child>.heading-element:first-child { + margin-top: 0 !important; +} + +.markdown-body .highlight pre:has(+.zeroclipboard-container) { + min-height: 52px; +} + diff --git a/tools/comment-preview/github-markdown-light.css b/tools/comment-preview/github-markdown-light.css new file mode 100644 index 0000000..35b318e --- /dev/null +++ b/tools/comment-preview/github-markdown-light.css @@ -0,0 +1,1124 @@ +/*light */ +.markdown-body { + color-scheme: light; + /** CSS default easing. Use for hover state changes and micro-interactions. */ + /** Accelerating motion. Use for elements exiting the viewport (moving off-screen). */ + /** Smooth acceleration and deceleration. Use for elements moving or morphing within the viewport. */ + /** Decelerating motion. Use for elements entering the viewport or appearing on screen. */ + /** Constant motion with no acceleration. Use for continuous animations like progress bars or loaders. */ + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + margin: 0; + font-weight: 400; + color: #1f2328; + background-color: #ffffff; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body a { + text-decoration: underline; + text-underline-offset: .2rem; +} + +.markdown-body .octicon { + display: inline-block; + fill: currentColor; + vertical-align: text-bottom; +} + +.markdown-body h1:hover .anchor .octicon-link:before, +.markdown-body h2:hover .anchor .octicon-link:before, +.markdown-body h3:hover .anchor .octicon-link:before, +.markdown-body h4:hover .anchor .octicon-link:before, +.markdown-body h5:hover .anchor .octicon-link:before, +.markdown-body h6:hover .anchor .octicon-link:before { + width: 16px; + height: 16px; + content: ' '; + display: inline-block; + background-color: currentColor; + -webkit-mask-image: url("data:image/svg+xml,"); + mask-image: url("data:image/svg+xml,"); +} + +.markdown-body details, +.markdown-body figcaption, +.markdown-body figure { + display: block; +} + +.markdown-body summary { + display: list-item; +} + +.markdown-body [hidden] { + display: none !important; +} + +.markdown-body a { + background-color: rgba(0,0,0,0); + color: #0969da; + text-decoration: none; +} + +.markdown-body abbr[title] { + border-bottom: none; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +.markdown-body b, +.markdown-body strong { + font-weight: 600; +} + +.markdown-body dfn { + font-style: italic; +} + +.markdown-body h1 { + margin: .67em 0; + font-weight: 600; + padding-bottom: .3em; + font-size: 2em; + border-bottom: 1px solid #d1d9e0b3; +} + +.markdown-body mark { + background-color: #fff8c5; + color: #1f2328; +} + +.markdown-body small { + font-size: 90%; +} + +.markdown-body sub, +.markdown-body sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +.markdown-body sub { + bottom: -0.25em; +} + +.markdown-body sup { + top: -0.5em; +} + +.markdown-body img { + border-style: none; + max-width: 100%; + box-sizing: content-box; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre, +.markdown-body samp { + font-family: monospace; + font-size: 1em; +} + +.markdown-body figure { + margin: 1em 2.5rem; +} + +.markdown-body hr { + box-sizing: content-box; + overflow: hidden; + background: rgba(0,0,0,0); + border-bottom: 1px solid #d1d9e0b3; + height: .25em; + padding: 0; + margin: 1.5rem 0; + background-color: #d1d9e0; + border: 0; +} + +.markdown-body input { + font: inherit; + margin: 0; + overflow: visible; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body [type=button], +.markdown-body [type=reset], +.markdown-body [type=submit] { + -webkit-appearance: button; + appearance: button; +} + +.markdown-body [type=checkbox], +.markdown-body [type=radio] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body [type=number]::-webkit-inner-spin-button, +.markdown-body [type=number]::-webkit-outer-spin-button { + height: auto; +} + +.markdown-body [type=search]::-webkit-search-cancel-button, +.markdown-body [type=search]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + +.markdown-body ::-webkit-input-placeholder { + color: inherit; + opacity: .54; +} + +.markdown-body ::-webkit-file-upload-button { + -webkit-appearance: button; + appearance: button; + font: inherit; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body ::placeholder { + color: #59636e; + opacity: 1; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; + display: block; + width: max-content; + max-width: 100%; + overflow: auto; + font-variant: tabular-nums; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body details summary { + cursor: pointer; +} + +.markdown-body a:focus, +.markdown-body [role=button]:focus, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=checkbox]:focus { + outline: 2px solid var(--borderColor-accent-emphasis); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:focus:not(:focus-visible), +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body input[type=radio]:focus:not(:focus-visible), +.markdown-body input[type=checkbox]:focus:not(:focus-visible) { + outline: solid 1px rgba(0,0,0,0); +} + +.markdown-body a:focus-visible, +.markdown-body [role=button]:focus-visible, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus-visible { + outline: 2px solid var(--borderColor-accent-emphasis); + outline-offset: -2px; + box-shadow: none; +} + +.markdown-body a:not([class]):focus, +.markdown-body a:not([class]):focus-visible, +.markdown-body input[type=radio]:focus, +.markdown-body input[type=radio]:focus-visible, +.markdown-body input[type=checkbox]:focus, +.markdown-body input[type=checkbox]:focus-visible { + outline-offset: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: 0.25rem; + font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + line-height: 10px; + color: #1f2328; + vertical-align: middle; + background-color: #f6f8fa; + border: solid 1px var(--borderColor-muted); + border-bottom-color: var(--borderColor-muted); + border-radius: 6px; + box-shadow: inset 0 -1px 0 var(--borderColor-muted); +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 1.5rem; + margin-bottom: 1rem; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h2 { + font-weight: 600; + padding-bottom: .3em; + font-size: 1.5em; + border-bottom: 1px solid #d1d9e0b3; +} + +.markdown-body h3 { + font-weight: 600; + font-size: 1.25em; +} + +.markdown-body h4 { + font-weight: 600; + font-size: 1em; +} + +.markdown-body h5 { + font-weight: 600; + font-size: .875em; +} + +.markdown-body h6 { + font-weight: 600; + font-size: .85em; + color: #59636e; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; + padding: 0 1em; + color: #59636e; + border-left: .25em solid #d1d9e0; +} + +.markdown-body ul, +.markdown-body ol { + margin-top: 0; + margin-bottom: 0; + padding-left: 2em; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body tt, +.markdown-body code, +.markdown-body samp { + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 12px; + word-wrap: normal; +} + +.markdown-body .octicon { + display: inline-block; + overflow: visible !important; + vertical-align: text-bottom; + fill: currentColor; +} + +.markdown-body input::-webkit-outer-spin-button, +.markdown-body input::-webkit-inner-spin-button { + margin: 0; + appearance: none; +} + +.markdown-body .mr-2 { + margin-right: 0.5rem !important; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .absent { + color: #d1242f; +} + +.markdown-body .anchor { + float: left; + padding-right: 0.25rem; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre, +.markdown-body details { + margin-top: 0; + margin-bottom: 1rem; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1f2328; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 tt, +.markdown-body h1 code, +.markdown-body h2 tt, +.markdown-body h2 code, +.markdown-body h3 tt, +.markdown-body h3 code, +.markdown-body h4 tt, +.markdown-body h4 code, +.markdown-body h5 tt, +.markdown-body h5 code, +.markdown-body h6 tt, +.markdown-body h6 code { + padding: 0 .2em; + font-size: inherit; +} + +.markdown-body summary h1, +.markdown-body summary h2, +.markdown-body summary h3, +.markdown-body summary h4, +.markdown-body summary h5, +.markdown-body summary h6 { + display: inline-block; +} + +.markdown-body summary h1 .anchor, +.markdown-body summary h2 .anchor, +.markdown-body summary h3 .anchor, +.markdown-body summary h4 .anchor, +.markdown-body summary h5 .anchor, +.markdown-body summary h6 .anchor { + margin-left: -40px; +} + +.markdown-body summary h1, +.markdown-body summary h2 { + padding-bottom: 0; + border-bottom: 0; +} + +.markdown-body ul.no-list, +.markdown-body ol.no-list { + padding: 0; + list-style-type: none; +} + +.markdown-body ol[type="a s"] { + list-style-type: lower-alpha; +} + +.markdown-body ol[type="A s"] { + list-style-type: upper-alpha; +} + +.markdown-body ol[type="i s"] { + list-style-type: lower-roman; +} + +.markdown-body ol[type="I s"] { + list-style-type: upper-roman; +} + +.markdown-body ol[type="1"] { + list-style-type: decimal; +} + +.markdown-body div>ol:not([type]) { + list-style-type: decimal; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li>p { + margin-top: 1rem; +} + +.markdown-body li+li { + margin-top: .25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 1rem; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 1rem; + margin-bottom: 1rem; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #d1d9e0; +} + +.markdown-body table td>:last-child { + margin-bottom: 0; +} + +.markdown-body table tr { + background-color: #ffffff; + border-top: 1px solid #d1d9e0b3; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body table img { + background-color: rgba(0,0,0,0); +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body .emoji { + max-width: none; + vertical-align: text-top; + background-color: rgba(0,0,0,0); +} + +.markdown-body span.frame { + display: block; + overflow: hidden; +} + +.markdown-body span.frame>span { + display: block; + float: left; + width: auto; + padding: 7px; + margin: 13px 0 0; + overflow: hidden; + border: 1px solid #d1d9e0; +} + +.markdown-body span.frame span img { + display: block; + float: left; +} + +.markdown-body span.frame span span { + display: block; + padding: 5px 0 0; + clear: both; + color: #1f2328; +} + +.markdown-body span.align-center { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-center>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: center; +} + +.markdown-body span.align-center span img { + margin: 0 auto; + text-align: center; +} + +.markdown-body span.align-right { + display: block; + overflow: hidden; + clear: both; +} + +.markdown-body span.align-right>span { + display: block; + margin: 13px 0 0; + overflow: hidden; + text-align: right; +} + +.markdown-body span.align-right span img { + margin: 0; + text-align: right; +} + +.markdown-body span.float-left { + display: block; + float: left; + margin-right: 13px; + overflow: hidden; +} + +.markdown-body span.float-left span { + margin: 13px 0 0; +} + +.markdown-body span.float-right { + display: block; + float: right; + margin-left: 13px; + overflow: hidden; +} + +.markdown-body span.float-right>span { + display: block; + margin: 13px auto 0; + overflow: hidden; + text-align: right; +} + +.markdown-body code, +.markdown-body tt { + padding: .2em .4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: #818b981f; + border-radius: 6px; +} + +.markdown-body code br, +.markdown-body tt br { + display: none; +} + +.markdown-body del code { + text-decoration: inherit; +} + +.markdown-body samp { + font-size: 85%; +} + +.markdown-body pre code { + font-size: 100%; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + word-break: normal; + white-space: pre; + background: rgba(0,0,0,0); + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 1rem; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 1rem; + overflow: auto; + font-size: 85%; + line-height: 1.45; + color: #1f2328; + background-color: #f6f8fa; + border-radius: 6px; +} + +.markdown-body pre code, +.markdown-body pre tt { + display: inline; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: rgba(0,0,0,0); + border: 0; +} + +.markdown-body .csv-data td, +.markdown-body .csv-data th { + padding: 5px; + overflow: hidden; + font-size: 12px; + line-height: 1; + text-align: left; + white-space: nowrap; +} + +.markdown-body .csv-data .blob-num { + padding: 10px 0.5rem 9px; + text-align: right; + background: #ffffff; + border: 0; +} + +.markdown-body .csv-data tr { + border-top: 0; +} + +.markdown-body .csv-data th { + font-weight: 600; + background: #f6f8fa; + border-top: 0; +} + +.markdown-body [data-footnote-ref]::before { + content: "["; +} + +.markdown-body [data-footnote-ref]::after { + content: "]"; +} + +.markdown-body .footnotes { + font-size: 12px; + color: #59636e; + border-top: 1px solid #d1d9e0; +} + +.markdown-body .footnotes ol { + padding-left: 1rem; +} + +.markdown-body .footnotes ol ul { + display: inline-block; + padding-left: 1rem; + margin-top: 1rem; +} + +.markdown-body .footnotes li { + position: relative; +} + +.markdown-body .footnotes li:target::before { + position: absolute; + top: calc(0.5rem*-1); + right: calc(0.5rem*-1); + bottom: calc(0.5rem*-1); + left: calc(1.5rem*-1); + pointer-events: none; + content: ""; + border: 2px solid #0969da; + border-radius: 6px; +} + +.markdown-body .footnotes li:target { + color: #1f2328; +} + +.markdown-body .footnotes .data-footnote-backref g-emoji { + font-family: monospace; +} + +.markdown-body .pl-c { + color: #59636e; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #0550ae; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6639ba; +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: #1f2328; +} + +.markdown-body .pl-ent { + color: #0550ae; +} + +.markdown-body .pl-k { + color: #cf222e; +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: #0a3069; +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: #953800; +} + +.markdown-body .pl-bu { + color: #82071e; +} + +.markdown-body .pl-ii { + color: var(--fgColor-danger); + background-color: var(--bgColor-danger-muted); +} + +.markdown-body .pl-c2 { + color: #f6f8fa; + background-color: #cf222e; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: #116329; +} + +.markdown-body .pl-ml { + color: #3b2300; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: #0550ae; +} + +.markdown-body .pl-mi { + font-style: italic; + color: #1f2328; +} + +.markdown-body .pl-mb { + font-weight: bold; + color: #1f2328; +} + +.markdown-body .pl-md { + color: #82071e; + background-color: #ffebe9; +} + +.markdown-body .pl-mi1 { + color: #116329; + background-color: #dafbe1; +} + +.markdown-body .pl-mc { + color: #953800; + background-color: #ffd8b5; +} + +.markdown-body .pl-mi2 { + color: #d1d9e0; + background-color: #0550ae; +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: #8250df; +} + +.markdown-body .pl-ba { + color: #59636e; +} + +.markdown-body .pl-sg { + color: #818b98; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #0a3069; +} + +.markdown-body [role=button]:focus:not(:focus-visible), +.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible), +.markdown-body button:focus:not(:focus-visible), +.markdown-body summary:focus:not(:focus-visible), +.markdown-body a:focus:not(:focus-visible) { + outline: none; + box-shadow: none; +} + +.markdown-body [tabindex="0"]:focus:not(:focus-visible), +.markdown-body details-dialog:focus:not(:focus-visible) { + outline: none; +} + +.markdown-body g-emoji { + display: inline-block; + min-width: 1ch; + font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; + font-size: 1em; + font-style: normal !important; + font-weight: 400; + line-height: 1; + vertical-align: -0.075em; +} + +.markdown-body g-emoji img { + width: 1em; + height: 1em; +} + +.markdown-body a:has(>p,>div,>pre,>blockquote) { + display: block; +} + +.markdown-body a:has(>p,>div,>pre,>blockquote):not(:has(.snippet-clipboard-content,>pre)) { + width: fit-content; +} + +.markdown-body a:has(>p,>div,>pre,>blockquote):has(.snippet-clipboard-content,>pre):focus-visible { + outline: 2px solid var(--borderColor-accent-emphasis); + outline-offset: 2px; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item label { + font-weight: 400; +} + +.markdown-body .task-list-item.enabled label { + cursor: pointer; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 0.25rem; +} + +.markdown-body .task-list-item .handle { + display: none; +} + +.markdown-body .task-list-item-checkbox { + margin: 0 .2em .25em -1.4em; + vertical-align: middle; +} + +.markdown-body ul:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body ol:dir(rtl) .task-list-item-checkbox { + margin: 0 -1.6em .25em .2em; +} + +.markdown-body .contains-task-list:hover .task-list-item-convert-container, +.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { + display: block; + width: auto; + height: 24px; + overflow: visible; + clip-path: none; +} + +.markdown-body ::-webkit-calendar-picker-indicator { + filter: invert(50%); +} + +.markdown-body .markdown-alert { + padding: 0.5rem 1rem; + margin-bottom: 1rem; + color: inherit; + border-left: .25em solid #d1d9e0; +} + +.markdown-body .markdown-alert>:first-child { + margin-top: 0; +} + +.markdown-body .markdown-alert>:last-child { + margin-bottom: 0; +} + +.markdown-body .markdown-alert .markdown-alert-title { + display: flex; + font-weight: 500; + align-items: center; + line-height: 1; +} + +.markdown-body .markdown-alert.markdown-alert-note { + border-left-color: #0969da; +} + +.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { + color: #0969da; +} + +.markdown-body .markdown-alert.markdown-alert-important { + border-left-color: #8250df; +} + +.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { + color: #8250df; +} + +.markdown-body .markdown-alert.markdown-alert-warning { + border-left-color: #9a6700; +} + +.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { + color: #9a6700; +} + +.markdown-body .markdown-alert.markdown-alert-tip { + border-left-color: #1a7f37; +} + +.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { + color: #1a7f37; +} + +.markdown-body .markdown-alert.markdown-alert-caution { + border-left-color: #cf222e; +} + +.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { + color: #d1242f; +} + +.markdown-body>*:first-child>.heading-element:first-child { + margin-top: 0 !important; +} + +.markdown-body .highlight pre:has(+.zeroclipboard-container) { + min-height: 52px; +} + diff --git a/tools/comment-preview/preview.ts b/tools/comment-preview/preview.ts new file mode 100644 index 0000000..9055d32 --- /dev/null +++ b/tools/comment-preview/preview.ts @@ -0,0 +1,96 @@ +// Render every shape of the PR comment, in light and dark, without a CI run. +// +// The comment is markdown that GitHub sanitises and styles, so rendering it any +// other way tells you nothing: half the constructs we rely on (``, ``, +// `align="absmiddle"`) survive or die by GitHub's rules, and the spacing comes +// from GitHub's own stylesheet. So this posts each fixture to GitHub's `/markdown` +// endpoint and drops the returned HTML into `github-markdown-css`. +// +// npm run preview # writes tools/comment-preview/out/index.html +// +// Needs `gh` authenticated. Nothing is posted anywhere — `/markdown` only renders. + +import { execFileSync } from "node:child_process"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import n from "nunjucks"; +import { fixtures } from "./fixtures.ts"; + +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = join(here, "..", ".."); +const outDir = join(here, "out"); + +const template = readFileSync(join(repoRoot, "src/reporters/github/success.md.j2"), "utf-8"); + +// Same configuration GithubReporter uses. Whitespace behaviour depends on it, and +// whitespace is most of what this tool exists to show. +n.configure({ autoescape: false, trimBlocks: true, lstripBlocks: true }); + +function renderMarkdown(markdown: string): string { + return execFileSync("gh", ["api", "-X", "POST", "/markdown", "--input", "-"], { + input: JSON.stringify({ mode: "gfm", text: markdown }), + encoding: "utf-8", + maxBuffer: 32 * 1024 * 1024, + }); +} + +const escape = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); + +mkdirSync(outDir, { recursive: true }); + +const sections = fixtures.map((fixture, i) => { + const markdown = n.renderString(template, fixture.view); + writeFileSync(join(outDir, `${String(i + 1).padStart(2, "0")}-${fixture.name.replace(/[^a-z0-9]+/gi, "-")}.md`), markdown); + const html = renderMarkdown(markdown); + return ` +
+

${escape(fixture.name)}

+

${escape(fixture.note)}

+
+
light
${html}
+
dark
${html}
+
+
`; +}); + +const light = readFileSync(join(here, "github-markdown-light.css"), "utf-8"); +const dark = readFileSync(join(here, "github-markdown-dark.css"), "utf-8"); + +const nav = fixtures.map((f, i) => `${escape(f.name)}`).join(""); + +writeFileSync( + join(outDir, "index.html"), + ` + +Query Doctor PR comment — every shape + + +${sections.join("\n")} +`, +); + +console.log(`${fixtures.length} fixtures → ${join(outDir, "index.html")}`);