<!-- GENERATED by scripts/build-llms.mjs from llms/conversation.md — do not edit this file. -->

# `lr-markdown-core`

- **Import** `import '@aceshooting/lyra-ui/components/lr-markdown-core.js';` (stable tag alias; registers the tag)
- **Class** `LyraMarkdownCore`, also available unregistered from `@aceshooting/lyra-ui/components/conversation/markdown/markdown-core.class.js`
- **Family** `components/conversation/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** `dompurify`, `katex`, `marked`, `shiki` — see `llms/peers.md`
- **Themeable via** 12 parts, 16 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-markdown-core`

A build-lean sibling of `<lr-markdown>` above, for a consumer whose `languages` map already covers
every language it will ever render — mirrors `<lr-code-block-core>`'s relationship to
`<lr-code-block>`. Where `<lr-markdown>` unconditionally calls `loadShikiHighlighter()` — the
default ~200-language dynamic-import table loader, whose bundled lookup table a bundler can't
statically narrow away —
this component's own module never imports or calls that function at all; it only ever calls
`loadShikiHighlighterCore(languages)`, so a consumer importing this entry point instead of
`markdown.js` gets a build genuinely free of shiki's full language table.

Removing `content` clears the document and its empty-document tab stop, including while streaming.
The property keeps Lit's `null` readback after removal; an explicitly empty attribute remains an
empty string. Later source text renders normally.

A fenced code block whose language isn't a key in `languages` always renders the plain-text fallback
— there is no default/full-table highlighter here to fall back to, the same default (not degraded)
rendering path as `<lr-code-block-core>`'s identical contract. A block that _is_ highlighted follows the
page's resolved theme through the same `[part="content"][data-dark-theme="true"]` hook `<lr-markdown>`
documents above, painting each token from `--shiki-dark`/`--shiki-dark-bg` on a dark palette. Every other capability — GFM tables,
links, blockquotes, images, heading anchors, `getHeadingTree()`, `fragment`/`text-quote` anchor-target
support (`highlights`, `activeHighlightId`, `scrollToAnchor()`, the `lr-highlight-activate`/
`lr-text-select`/`lr-anchor-result` events), math via the optional `katex` peer, the sanitize/
`htmlMode`/streaming fallback matrix and known gotchas — is identical to `<lr-markdown>`; see that
section above for the full write-up of shared behavior.

**Properties:** `content: string = ''`, `tabSize: number = 4` (attribute `tab-size`) — the same
finite-integer-guarded leading-indentation expansion used by `<lr-markdown>`; finite values are
clamped to `[1, 32]` (`0` becomes `1`, `33` becomes `32`), and only non-finite values (`NaN`,
`Infinity`) fall back to `4`, independently of rendered code's
`--lr-code-block-tab-size`; `marked: LyraMarkedParser | undefined` (readonly, no attribute) — this
instance's isolated peer-neutral configurable parser; `htmlMode: 'sanitize' | 'escape' | 'trusted' =
'sanitize'` (attribute `html-mode`), `gfm: boolean = true`, `linkTarget: string | null = '_blank'` (attribute
`link-target`), `internalLinkPrefix: string = ''` (attribute `internal-link-prefix`),
`headingOffset: number = 0` (attribute `heading-offset`), `streaming: boolean = false` (reflected),
`highlightCode: boolean = true` (attribute
`highlight-code`), `languages: Record<string, ShikiLanguageSource> = {}` (attribute: false) —
required, unlike `<lr-markdown>`'s optional `languages?:`; empty (the default) means every fenced
block stays unhighlighted permanently. Each value is either an already-resolved grammar or a lazy
loader (`() => Promise<ShikiLanguageInput | { default: ShikiLanguageInput }>`, e.g.
`() => import('@shikijs/langs/bash')`) — called at most once per key, memoized, the first time a
fenced block actually requests that language, `headingAnchors: boolean = false` (attribute `heading-anchors`),
`math: boolean = false`, `maxHeight: string = ''` (attribute `max-height`) — same CSS-length scroll
cap on `[part="content"]` as `<lr-markdown>`'s own; plus the same inherited anchor-target properties as `<lr-markdown>`:
`highlights: readonly LyraHighlight[] = []` (attribute: false), `activeHighlightId: string | null = null`
(attribute `active-highlight-id`), `anchor: LyraAnchor | string | null = null` (attribute: false),
and `anchorKinds: readonly ('fragment' | 'text-quote')[] = ['fragment', 'text-quote']`.
The same 1,000,000-code-unit/20,000-node corpus ceiling, 4,096-code-unit quote-field ceiling,
4,000,000-code-unit work ceiling, 10,000-record admission ceiling, 1,000-candidate window, and
100-painted-highlight limit documented for `<lr-markdown>` apply here too, including active-first
retention from anywhere in the admitted snapshot.

**Methods:** `renderMarkdown(): void` — immediately reruns the current content through the parse,
sanitize, highlight, and fallback pipeline after changing this instance's `marked` configuration;
safely no-ops while the parser is unresolved. `refreshTheme(): void` re-reads the resolved theme
for syntax highlighting. `getHeadingTree()` — same contract as
`<lr-markdown>`'s own. `LyraMarkdownCore.getMarked(): Marked` and
`LyraMarkdownCore.updateAll(): void` provide the same variant-scoped compatibility-parser contract
as the full class; the core route exports its own `Marked` alias.

**Events:** `lr-link-click`, `lr-render-error`, `lr-highlight-activate`, `lr-text-select`,
`lr-anchor-result`, `lr-content-settled` — identical detail shapes to `<lr-markdown>`'s own.

**Slots:** none — content comes from the `content` property, not light-DOM children.

**CSS parts:** `anchor-live-region` (the aria-hidden, non-live shadow mirror of the latest
anchor-jump message), `content` (respects `max-height`), `heading`, `paragraph`, `list`,
`code-block`, `inline-code`, `link`, `table`, `blockquote`, `img`, `math` — identical to
`<lr-markdown>`'s own parts.

**Themeable custom properties:** identical to `<lr-markdown>`'s own tokens, including
`--lr-markdown-max-height` (default `none` — cap on `[part="content"]`'s block size; the
`maxHeight` property sets this token inline) and its code-surface tokens —
`--lr-markdown-code-bg` (default `var(--lr-color-brand-quiet)`, shared by inline `code` and the
fenced `code-block` surface), `--lr-markdown-code-padding`/`--lr-markdown-code-radius` (inline
`code` span padding/radius, defaulting to `var(--lr-size-0-125rem) var(--lr-size-0-3125rem)`/
`calc(var(--lr-radius) * 0.5)`), and `--lr-markdown-code-block-padding`/
`--lr-markdown-code-block-radius` (the fenced `code-block` surface's padding/radius, defaulting to
`var(--lr-space-s) var(--lr-space-m)`/`var(--lr-radius)`) — plus `--lr-code-block-tab-size` (default
`2` — tab width inside a rendered fenced or indented `code-block`), with exactly the mechanics
described under `<lr-markdown>` above: the same property name and default that
`<lr-code-block>`/`<lr-code-editor>` read, declared as a `var()` fallback at the point of use rather
than on `:host` so a page- or container-level value reaches it, and carried here in its own right
because this element is a **sibling** of `<lr-code-block>` rather than an ancestor of it. Markdown
code blocks wrap (`white-space: pre-wrap`) while `<lr-code-block>` does not, so the same tab width
can render differently on a wrapped line. `--lr-markdown-font-mono` is the monospace stack used by
rendered code and defaults to `var(--lr-font-mono)`.

**Optional peer deps:** `marked`, `dompurify` (both lazy-loaded, same as `<lr-markdown>`), `katex`
(for `math`). Does _not_ depend on the full `shiki` package's default entry point — only
`shiki/core`, `shiki/engine/oniguruma`, the binary `shiki/onig.wasm` asset (the default; select
`shiki/engine/javascript` instead via `setShikiCoreEngine('javascript')`), and
`shiki/themes/github-{light,dark}.mjs`, the same fine-grained subset `<lr-code-block-core>` depends
on.

````ts
import { html } from "lit";
import python from "shiki/langs/python.mjs";
import "@aceshooting/lyra-ui/components/conversation/markdown/markdown-core.js";

const view = html`<lr-markdown-core
  .content=${"# Report\n\n```python\nprint('hi')\n```\n\n```bash\necho hi\n```"}
  .languages=${{ python, bash: () => import("shiki/langs/bash.mjs") }}
></lr-markdown-core>`;
````

**Additional API surface:**

- `--lr-markdown-table-header-bg` — Background of every rendered `[part="table"]` header cell. Default: `var(--lr-color-brand-quiet)`.
- `--lr-markdown-highlight-accent-bg` — Accent highlight fill. Default: `var(--lr-color-brand-quiet)`.
- `--lr-markdown-highlight-success-bg` — Success highlight fill. Default: `var(--lr-color-success-quiet)`.
- `--lr-markdown-highlight-warning-bg` — Warning highlight fill. Default: `var(--lr-color-warning-quiet)`.
- `--lr-markdown-highlight-danger-bg` — Danger highlight fill. Default: `var(--lr-color-danger-quiet)`.
- `--lr-markdown-highlight-neutral-bg` — Neutral highlight fill. Default: `var(--lr-color-surface)`.
- `--lr-markdown-highlight-active-bg` — Active highlight fill. Default: `var(--lr-color-brand-quiet)`.
- `--lr-markdown-highlight-active-outline-color` — Active highlight outline. Default: `var(--lr-color-brand)`.

---
