New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

flatmarkdown-ast2angular

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

flatmarkdown-ast2angular - npm Package Compare versions

Comparing version
0.3.0
to
0.3.1
+786
fesm2022/flatmarkdown-ast2angular.mjs
import * as i0 from '@angular/core';
import { input, computed, ChangeDetectionStrategy, Component } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { RouterLink } from '@angular/router';
function defaultContext(options = {}) {
return {
options,
inHeaderRow: false,
tableAlignments: [],
cellIndex: 0,
tightList: false,
};
}
function extractText(node) {
if (node.type === 'text')
return node.value;
if ('children' in node && node.children) {
return node.children.map(extractText).join('');
}
return '';
}
class FmNodeComponent {
node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
context = input.required(...(ngDevMode ? [{ debugName: "context" }] : []));
children = computed(() => {
const n = this.node();
return 'children' in n && n.children ? n.children : [];
}, ...(ngDevMode ? [{ debugName: "children" }] : []));
cellAlignment = computed(() => {
const ctx = this.context();
const alignment = ctx.tableAlignments[ctx.cellIndex];
return alignment && alignment !== 'none' ? alignment : null;
}, ...(ngDevMode ? [{ debugName: "cellAlignment" }] : []));
altText = computed(() => extractText(this.node()), ...(ngDevMode ? [{ debugName: "altText" }] : []));
wikilinkRoute = computed(() => {
const n = this.node();
const prefix = this.context().options.wikilink?.routerLinkPrefix ?? '';
return prefix + n.url;
}, ...(ngDevMode ? [{ debugName: "wikilinkRoute" }] : []));
hashtagRoute = computed(() => {
const n = this.node();
const prefix = this.context().options.hashtag?.routerLinkPrefix ?? '';
return prefix + n.value;
}, ...(ngDevMode ? [{ debugName: "hashtagRoute" }] : []));
childContext(overrides) {
return { ...this.context(), ...overrides };
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FmNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FmNodeComponent, isStandalone: true, selector: "fm-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, context: { classPropertyName: "context", publicName: "context", isSignal: true, isRequired: true, transformFunction: null } }, host: { styleAttribute: "display:contents" }, ngImport: i0, template: `
@switch (node().type) {
<!-- ── Block Nodes ── -->
@case ('document') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('paragraph') {
@if (context().tightList) {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
} @else {
<p>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</p>
}
}
@case ('heading') {
<ng-template #headingContent>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ng-template>
@switch ($any(node()).level) {
@case (1) { <h1><ng-container *ngTemplateOutlet="headingContent" /></h1> }
@case (2) { <h2><ng-container *ngTemplateOutlet="headingContent" /></h2> }
@case (3) { <h3><ng-container *ngTemplateOutlet="headingContent" /></h3> }
@case (4) { <h4><ng-container *ngTemplateOutlet="headingContent" /></h4> }
@case (5) { <h5><ng-container *ngTemplateOutlet="headingContent" /></h5> }
@case (6) { <h6><ng-container *ngTemplateOutlet="headingContent" /></h6> }
}
}
@case ('code_block') {
<pre><code [class]="$any(node()).info ? 'language-' + $any(node()).info : null">{{ $any(node()).literal }}</code></pre>
}
@case ('block_quote') {
<blockquote>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</blockquote>
}
@case ('multiline_block_quote') {
<blockquote>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</blockquote>
}
@case ('list') {
@if ($any(node()).list_type === 'ordered') {
<ol [attr.start]="$any(node()).start != null && $any(node()).start !== 1 ? $any(node()).start : null">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ol>
} @else {
<ul>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ul>
}
}
@case ('item') {
<li>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tightList: $any(node()).tight })" />
}
</li>
}
@case ('task_item') {
<li>
<input type="checkbox" [checked]="$any(node()).symbol != null" disabled />
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tightList: true })" />
}
</li>
}
@case ('table') {
<table>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tableAlignments: $any(node()).alignments })" />
}
</table>
}
@case ('table_row') {
<tr>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ inHeaderRow: $any(node()).header, cellIndex: $index })" />
}
</tr>
}
@case ('table_cell') {
@if (context().inHeaderRow) {
<th [attr.align]="cellAlignment()">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</th>
} @else {
<td [attr.align]="cellAlignment()">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</td>
}
}
@case ('thematic_break') {
<hr />
}
@case ('html_block') {
<span [innerHTML]="$any(node()).literal"></span>
}
@case ('footnote_definition') {
<div class="footnote" [id]="'fn-' + $any(node()).name">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</div>
}
@case ('description_list') {
<dl>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dl>
}
@case ('description_item') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('description_term') {
<dt>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dt>
}
@case ('description_details') {
<dd>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dd>
}
@case ('alert') {
<div [class]="'alert alert-' + $any(node()).alert_type">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</div>
}
@case ('front_matter') {
<!-- front_matter is not rendered -->
}
@case ('heex_block') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('subtext') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
<!-- ── Inline Nodes ── -->
@case ('text') {
{{ $any(node()).value }}
}
@case ('softbreak') {
{{ '\n' }}
}
@case ('linebreak') {
<br />
}
@case ('emph') {
<em>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</em>
}
@case ('strong') {
<strong>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</strong>
}
@case ('strikethrough') {
<del>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</del>
}
@case ('underline') {
<u>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</u>
}
@case ('highlight') {
<mark>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</mark>
}
@case ('superscript') {
<sup>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</sup>
}
@case ('subscript') {
<sub>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</sub>
}
@case ('spoilered_text') {
<span class="spoiler">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</span>
}
@case ('code') {
<code>{{ $any(node()).literal }}</code>
}
@case ('link') {
<a [href]="$any(node()).url" [attr.title]="$any(node()).title || null">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</a>
}
@case ('image') {
<img [src]="$any(node()).url" [alt]="altText()" [attr.title]="$any(node()).title || null" />
}
@case ('wikilink') {
<a [routerLink]="wikilinkRoute()"
[class]="context().options.wikilink?.cssClass || null"
data-wikilink="true">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</a>
}
@case ('hashtag') {
<a [routerLink]="hashtagRoute()"
[class]="context().options.hashtag?.cssClass || null"
data-hashtag="true">{{ $any(node()).value }}</a>
}
@case ('footnote_reference') {
<sup><a [href]="'#fn-' + $any(node()).name">{{ '[' + ($any(node()).ix + 1) + ']' }}</a></sup>
}
@case ('shortcode') {
{{ $any(node()).emoji }}
}
@case ('math') {
@if ($any(node()).display_math) {
<pre><code class="math math-display">{{ $any(node()).literal }}</code></pre>
} @else {
<code class="math math-inline">{{ $any(node()).literal }}</code>
}
}
@case ('html_inline') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('heex_inline') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('raw') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('escaped') {
<!-- escaped produces no output -->
}
@case ('escaped_tag') {
{{ $any(node()).value }}
}
}
`, isInline: true, dependencies: [{ kind: "component", type: FmNodeComponent, selector: "fm-node", inputs: ["node", "context"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FmNodeComponent, decorators: [{
type: Component,
args: [{
selector: 'fm-node',
standalone: true,
imports: [NgTemplateOutlet, RouterLink],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { style: 'display:contents' },
template: `
@switch (node().type) {
<!-- ── Block Nodes ── -->
@case ('document') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('paragraph') {
@if (context().tightList) {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
} @else {
<p>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</p>
}
}
@case ('heading') {
<ng-template #headingContent>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ng-template>
@switch ($any(node()).level) {
@case (1) { <h1><ng-container *ngTemplateOutlet="headingContent" /></h1> }
@case (2) { <h2><ng-container *ngTemplateOutlet="headingContent" /></h2> }
@case (3) { <h3><ng-container *ngTemplateOutlet="headingContent" /></h3> }
@case (4) { <h4><ng-container *ngTemplateOutlet="headingContent" /></h4> }
@case (5) { <h5><ng-container *ngTemplateOutlet="headingContent" /></h5> }
@case (6) { <h6><ng-container *ngTemplateOutlet="headingContent" /></h6> }
}
}
@case ('code_block') {
<pre><code [class]="$any(node()).info ? 'language-' + $any(node()).info : null">{{ $any(node()).literal }}</code></pre>
}
@case ('block_quote') {
<blockquote>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</blockquote>
}
@case ('multiline_block_quote') {
<blockquote>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</blockquote>
}
@case ('list') {
@if ($any(node()).list_type === 'ordered') {
<ol [attr.start]="$any(node()).start != null && $any(node()).start !== 1 ? $any(node()).start : null">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ol>
} @else {
<ul>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ul>
}
}
@case ('item') {
<li>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tightList: $any(node()).tight })" />
}
</li>
}
@case ('task_item') {
<li>
<input type="checkbox" [checked]="$any(node()).symbol != null" disabled />
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tightList: true })" />
}
</li>
}
@case ('table') {
<table>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tableAlignments: $any(node()).alignments })" />
}
</table>
}
@case ('table_row') {
<tr>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ inHeaderRow: $any(node()).header, cellIndex: $index })" />
}
</tr>
}
@case ('table_cell') {
@if (context().inHeaderRow) {
<th [attr.align]="cellAlignment()">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</th>
} @else {
<td [attr.align]="cellAlignment()">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</td>
}
}
@case ('thematic_break') {
<hr />
}
@case ('html_block') {
<span [innerHTML]="$any(node()).literal"></span>
}
@case ('footnote_definition') {
<div class="footnote" [id]="'fn-' + $any(node()).name">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</div>
}
@case ('description_list') {
<dl>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dl>
}
@case ('description_item') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('description_term') {
<dt>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dt>
}
@case ('description_details') {
<dd>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dd>
}
@case ('alert') {
<div [class]="'alert alert-' + $any(node()).alert_type">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</div>
}
@case ('front_matter') {
<!-- front_matter is not rendered -->
}
@case ('heex_block') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('subtext') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
<!-- ── Inline Nodes ── -->
@case ('text') {
{{ $any(node()).value }}
}
@case ('softbreak') {
{{ '\n' }}
}
@case ('linebreak') {
<br />
}
@case ('emph') {
<em>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</em>
}
@case ('strong') {
<strong>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</strong>
}
@case ('strikethrough') {
<del>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</del>
}
@case ('underline') {
<u>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</u>
}
@case ('highlight') {
<mark>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</mark>
}
@case ('superscript') {
<sup>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</sup>
}
@case ('subscript') {
<sub>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</sub>
}
@case ('spoilered_text') {
<span class="spoiler">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</span>
}
@case ('code') {
<code>{{ $any(node()).literal }}</code>
}
@case ('link') {
<a [href]="$any(node()).url" [attr.title]="$any(node()).title || null">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</a>
}
@case ('image') {
<img [src]="$any(node()).url" [alt]="altText()" [attr.title]="$any(node()).title || null" />
}
@case ('wikilink') {
<a [routerLink]="wikilinkRoute()"
[class]="context().options.wikilink?.cssClass || null"
data-wikilink="true">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</a>
}
@case ('hashtag') {
<a [routerLink]="hashtagRoute()"
[class]="context().options.hashtag?.cssClass || null"
data-hashtag="true">{{ $any(node()).value }}</a>
}
@case ('footnote_reference') {
<sup><a [href]="'#fn-' + $any(node()).name">{{ '[' + ($any(node()).ix + 1) + ']' }}</a></sup>
}
@case ('shortcode') {
{{ $any(node()).emoji }}
}
@case ('math') {
@if ($any(node()).display_math) {
<pre><code class="math math-display">{{ $any(node()).literal }}</code></pre>
} @else {
<code class="math math-inline">{{ $any(node()).literal }}</code>
}
}
@case ('html_inline') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('heex_inline') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('raw') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('escaped') {
<!-- escaped produces no output -->
}
@case ('escaped_tag') {
{{ $any(node()).value }}
}
}
`,
}]
}], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], context: [{ type: i0.Input, args: [{ isSignal: true, alias: "context", required: true }] }] } });
class FmRootComponent {
ast = input.required(...(ngDevMode ? [{ debugName: "ast" }] : []));
options = input({}, ...(ngDevMode ? [{ debugName: "options" }] : []));
rootNode = computed(() => {
const v = this.ast();
if (!v)
return null;
return typeof v === 'string' ? JSON.parse(v) : v;
}, ...(ngDevMode ? [{ debugName: "rootNode" }] : []));
rootContext = computed(() => defaultContext(this.options()), ...(ngDevMode ? [{ debugName: "rootContext" }] : []));
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FmRootComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.0", type: FmRootComponent, isStandalone: true, selector: "fm-root", inputs: { ast: { classPropertyName: "ast", publicName: "ast", isSignal: true, isRequired: true, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null } }, host: { styleAttribute: "display:contents" }, ngImport: i0, template: `
@if (rootNode(); as root) {
<fm-node [node]="root" [context]="rootContext()" />
}
`, isInline: true, dependencies: [{ kind: "component", type: FmNodeComponent, selector: "fm-node", inputs: ["node", "context"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.0", ngImport: i0, type: FmRootComponent, decorators: [{
type: Component,
args: [{
selector: 'fm-root',
standalone: true,
imports: [FmNodeComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { style: 'display:contents' },
template: `
@if (rootNode(); as root) {
<fm-node [node]="root" [context]="rootContext()" />
}
`,
}]
}], propDecorators: { ast: [{ type: i0.Input, args: [{ isSignal: true, alias: "ast", required: true }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }] } });
/**
* Generated bundle index. Do not edit.
*/
export { FmNodeComponent, FmRootComponent };
//# sourceMappingURL=flatmarkdown-ast2angular.mjs.map
{"version":3,"file":"flatmarkdown-ast2angular.mjs","sources":["../../src/lib/types.ts","../../src/lib/utils.ts","../../src/lib/fm-node.component.ts","../../src/lib/fm-root.component.ts","../../src/flatmarkdown-ast2angular.ts"],"sourcesContent":["import type { AstNode } from 'flatmarkdown-ast';\n\nexport interface WikiLinkAngularOptions {\n /** Prefix prepended to the wikilink url for routerLink. Default: '' */\n routerLinkPrefix?: string;\n /** CSS class applied to wikilink anchors. */\n cssClass?: string;\n}\n\nexport interface HashtagAngularOptions {\n /** Prefix prepended to the hashtag value for routerLink. Default: '' */\n routerLinkPrefix?: string;\n /** CSS class applied to hashtag anchors. */\n cssClass?: string;\n}\n\nexport interface AngularRenderOptions {\n wikilink?: WikiLinkAngularOptions;\n hashtag?: HashtagAngularOptions;\n}\n\nexport interface RenderContext {\n options: AngularRenderOptions;\n inHeaderRow: boolean;\n tableAlignments: string[];\n cellIndex: number;\n tightList: boolean;\n}\n\nexport function defaultContext(options: AngularRenderOptions = {}): RenderContext {\n return {\n options,\n inHeaderRow: false,\n tableAlignments: [],\n cellIndex: 0,\n tightList: false,\n };\n}\n","import type { AstNode } from 'flatmarkdown-ast';\n\nexport function extractText(node: AstNode): string {\n if (node.type === 'text') return node.value;\n if ('children' in node && node.children) {\n return node.children.map(extractText).join('');\n }\n return '';\n}\n","import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { RouterLink } from '@angular/router';\nimport type { AstNode } from 'flatmarkdown-ast';\nimport { RenderContext } from './types';\nimport { extractText } from './utils';\n\n@Component({\n selector: 'fm-node',\n standalone: true,\n imports: [NgTemplateOutlet, RouterLink],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { style: 'display:contents' },\n template: `\n @switch (node().type) {\n <!-- ── Block Nodes ── -->\n\n @case ('document') {\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n }\n\n @case ('paragraph') {\n @if (context().tightList) {\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n } @else {\n <p>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </p>\n }\n }\n\n @case ('heading') {\n <ng-template #headingContent>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </ng-template>\n @switch ($any(node()).level) {\n @case (1) { <h1><ng-container *ngTemplateOutlet=\"headingContent\" /></h1> }\n @case (2) { <h2><ng-container *ngTemplateOutlet=\"headingContent\" /></h2> }\n @case (3) { <h3><ng-container *ngTemplateOutlet=\"headingContent\" /></h3> }\n @case (4) { <h4><ng-container *ngTemplateOutlet=\"headingContent\" /></h4> }\n @case (5) { <h5><ng-container *ngTemplateOutlet=\"headingContent\" /></h5> }\n @case (6) { <h6><ng-container *ngTemplateOutlet=\"headingContent\" /></h6> }\n }\n }\n\n @case ('code_block') {\n <pre><code [class]=\"$any(node()).info ? 'language-' + $any(node()).info : null\">{{ $any(node()).literal }}</code></pre>\n }\n\n @case ('block_quote') {\n <blockquote>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </blockquote>\n }\n\n @case ('multiline_block_quote') {\n <blockquote>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </blockquote>\n }\n\n @case ('list') {\n @if ($any(node()).list_type === 'ordered') {\n <ol [attr.start]=\"$any(node()).start != null && $any(node()).start !== 1 ? $any(node()).start : null\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </ol>\n } @else {\n <ul>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </ul>\n }\n }\n\n @case ('item') {\n <li>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"childContext({ tightList: $any(node()).tight })\" />\n }\n </li>\n }\n\n @case ('task_item') {\n <li>\n <input type=\"checkbox\" [checked]=\"$any(node()).symbol != null\" disabled />\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"childContext({ tightList: true })\" />\n }\n </li>\n }\n\n @case ('table') {\n <table>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"childContext({ tableAlignments: $any(node()).alignments })\" />\n }\n </table>\n }\n\n @case ('table_row') {\n <tr>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"childContext({ inHeaderRow: $any(node()).header, cellIndex: $index })\" />\n }\n </tr>\n }\n\n @case ('table_cell') {\n @if (context().inHeaderRow) {\n <th [attr.align]=\"cellAlignment()\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </th>\n } @else {\n <td [attr.align]=\"cellAlignment()\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </td>\n }\n }\n\n @case ('thematic_break') {\n <hr />\n }\n\n @case ('html_block') {\n <span [innerHTML]=\"$any(node()).literal\"></span>\n }\n\n @case ('footnote_definition') {\n <div class=\"footnote\" [id]=\"'fn-' + $any(node()).name\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </div>\n }\n\n @case ('description_list') {\n <dl>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </dl>\n }\n\n @case ('description_item') {\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n }\n\n @case ('description_term') {\n <dt>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </dt>\n }\n\n @case ('description_details') {\n <dd>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </dd>\n }\n\n @case ('alert') {\n <div [class]=\"'alert alert-' + $any(node()).alert_type\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </div>\n }\n\n @case ('front_matter') {\n <!-- front_matter is not rendered -->\n }\n\n @case ('heex_block') {\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n }\n\n @case ('subtext') {\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n }\n\n <!-- ── Inline Nodes ── -->\n\n @case ('text') {\n {{ $any(node()).value }}\n }\n\n @case ('softbreak') {\n {{ '\\n' }}\n }\n\n @case ('linebreak') {\n <br />\n }\n\n @case ('emph') {\n <em>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </em>\n }\n\n @case ('strong') {\n <strong>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </strong>\n }\n\n @case ('strikethrough') {\n <del>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </del>\n }\n\n @case ('underline') {\n <u>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </u>\n }\n\n @case ('highlight') {\n <mark>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </mark>\n }\n\n @case ('superscript') {\n <sup>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </sup>\n }\n\n @case ('subscript') {\n <sub>\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </sub>\n }\n\n @case ('spoilered_text') {\n <span class=\"spoiler\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </span>\n }\n\n @case ('code') {\n <code>{{ $any(node()).literal }}</code>\n }\n\n @case ('link') {\n <a [href]=\"$any(node()).url\" [attr.title]=\"$any(node()).title || null\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </a>\n }\n\n @case ('image') {\n <img [src]=\"$any(node()).url\" [alt]=\"altText()\" [attr.title]=\"$any(node()).title || null\" />\n }\n\n @case ('wikilink') {\n <a [routerLink]=\"wikilinkRoute()\"\n [class]=\"context().options.wikilink?.cssClass || null\"\n data-wikilink=\"true\">\n @for (child of children(); track $index) {\n <fm-node [node]=\"child\" [context]=\"context()\" />\n }\n </a>\n }\n\n @case ('hashtag') {\n <a [routerLink]=\"hashtagRoute()\"\n [class]=\"context().options.hashtag?.cssClass || null\"\n data-hashtag=\"true\">{{ $any(node()).value }}</a>\n }\n\n @case ('footnote_reference') {\n <sup><a [href]=\"'#fn-' + $any(node()).name\">{{ '[' + ($any(node()).ix + 1) + ']' }}</a></sup>\n }\n\n @case ('shortcode') {\n {{ $any(node()).emoji }}\n }\n\n @case ('math') {\n @if ($any(node()).display_math) {\n <pre><code class=\"math math-display\">{{ $any(node()).literal }}</code></pre>\n } @else {\n <code class=\"math math-inline\">{{ $any(node()).literal }}</code>\n }\n }\n\n @case ('html_inline') {\n <span [innerHTML]=\"$any(node()).value\"></span>\n }\n\n @case ('heex_inline') {\n <span [innerHTML]=\"$any(node()).value\"></span>\n }\n\n @case ('raw') {\n <span [innerHTML]=\"$any(node()).value\"></span>\n }\n\n @case ('escaped') {\n <!-- escaped produces no output -->\n }\n\n @case ('escaped_tag') {\n {{ $any(node()).value }}\n }\n }\n `,\n})\nexport class FmNodeComponent {\n readonly node = input.required<AstNode>();\n readonly context = input.required<RenderContext>();\n\n readonly children = computed(() => {\n const n = this.node();\n return 'children' in n && n.children ? n.children : [];\n });\n\n readonly cellAlignment = computed(() => {\n const ctx = this.context();\n const alignment = ctx.tableAlignments[ctx.cellIndex];\n return alignment && alignment !== 'none' ? alignment : null;\n });\n\n readonly altText = computed(() => extractText(this.node()));\n\n readonly wikilinkRoute = computed(() => {\n const n = this.node() as { url: string };\n const prefix = this.context().options.wikilink?.routerLinkPrefix ?? '';\n return prefix + n.url;\n });\n\n readonly hashtagRoute = computed(() => {\n const n = this.node() as { value: string };\n const prefix = this.context().options.hashtag?.routerLinkPrefix ?? '';\n return prefix + n.value;\n });\n\n childContext(overrides: Partial<RenderContext>): RenderContext {\n return { ...this.context(), ...overrides };\n }\n}\n","import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';\nimport type { AstNode } from 'flatmarkdown-ast';\nimport { AngularRenderOptions, RenderContext, defaultContext } from './types';\nimport { FmNodeComponent } from './fm-node.component';\n\n@Component({\n selector: 'fm-root',\n standalone: true,\n imports: [FmNodeComponent],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { style: 'display:contents' },\n template: `\n @if (rootNode(); as root) {\n <fm-node [node]=\"root\" [context]=\"rootContext()\" />\n }\n `,\n})\nexport class FmRootComponent {\n readonly ast = input.required<AstNode | string>();\n readonly options = input<AngularRenderOptions>({});\n\n readonly rootNode = computed<AstNode | null>(() => {\n const v = this.ast();\n if (!v) return null;\n return typeof v === 'string' ? JSON.parse(v) : v;\n });\n\n readonly rootContext = computed<RenderContext>(() => defaultContext(this.options()));\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
import * as _angular_core from '@angular/core';
import { AstNode } from 'flatmarkdown-ast';
interface WikiLinkAngularOptions {
/** Prefix prepended to the wikilink url for routerLink. Default: '' */
routerLinkPrefix?: string;
/** CSS class applied to wikilink anchors. */
cssClass?: string;
}
interface HashtagAngularOptions {
/** Prefix prepended to the hashtag value for routerLink. Default: '' */
routerLinkPrefix?: string;
/** CSS class applied to hashtag anchors. */
cssClass?: string;
}
interface AngularRenderOptions {
wikilink?: WikiLinkAngularOptions;
hashtag?: HashtagAngularOptions;
}
interface RenderContext {
options: AngularRenderOptions;
inHeaderRow: boolean;
tableAlignments: string[];
cellIndex: number;
tightList: boolean;
}
declare class FmRootComponent {
readonly ast: _angular_core.InputSignal<string | AstNode>;
readonly options: _angular_core.InputSignal<AngularRenderOptions>;
readonly rootNode: _angular_core.Signal<AstNode>;
readonly rootContext: _angular_core.Signal<RenderContext>;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FmRootComponent, never>;
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FmRootComponent, "fm-root", never, { "ast": { "alias": "ast"; "required": true; "isSignal": true; }; "options": { "alias": "options"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
}
declare class FmNodeComponent {
readonly node: _angular_core.InputSignal<AstNode>;
readonly context: _angular_core.InputSignal<RenderContext>;
readonly children: _angular_core.Signal<AstNode[]>;
readonly cellAlignment: _angular_core.Signal<string>;
readonly altText: _angular_core.Signal<string>;
readonly wikilinkRoute: _angular_core.Signal<string>;
readonly hashtagRoute: _angular_core.Signal<string>;
childContext(overrides: Partial<RenderContext>): RenderContext;
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FmNodeComponent, never>;
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FmNodeComponent, "fm-node", never, { "node": { "alias": "node"; "required": true; "isSignal": true; }; "context": { "alias": "context"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
}
export { FmNodeComponent, FmRootComponent };
export type { AngularRenderOptions, RenderContext, WikiLinkAngularOptions };
+16
-22
{
"name": "flatmarkdown-ast2angular",
"version": "0.3.0",
"version": "0.3.1",
"description": "Render FlatMarkdown AST as native Angular components with routerLink support for wikilinks.",

@@ -24,7 +24,2 @@ "repository": {

"license": "MIT",
"scripts": {
"build": "ng-packagr -p ng-package.json",
"test": "vitest run",
"publish:npm": "npm run build && cd dist && npm publish"
},
"peerDependencies": {

@@ -36,18 +31,17 @@ "@angular/common": "^21.0.0",

},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^2.3.0",
"@angular/build": "^21.0.0",
"@angular/common": "^21.0.0",
"@angular/compiler": "^21.0.0",
"@angular/compiler-cli": "^21.0.0",
"@angular/core": "^21.0.0",
"@angular/platform-browser": "^21.0.0",
"@angular/platform-browser-dynamic": "^21.0.0",
"@angular/router": "^21.0.0",
"flatmarkdown-ast": "^0.2.0",
"jsdom": "^28.1.0",
"ng-packagr": "^21.0.0",
"typescript": "^5.8.0",
"vitest": "^4.0.8"
"module": "fesm2022/flatmarkdown-ast2angular.mjs",
"typings": "types/flatmarkdown-ast2angular.d.ts",
"exports": {
"./package.json": {
"default": "./package.json"
},
".": {
"types": "./types/flatmarkdown-ast2angular.d.ts",
"default": "./fesm2022/flatmarkdown-ast2angular.mjs"
}
},
"sideEffects": false,
"dependencies": {
"tslib": "^2.3.0"
}
}
}
{
"$schema": "https://raw.githubusercontent.com/ng-packagr/ng-packagr/master/src/ng-package.schema.json",
"dest": "dist",
"lib": {
"entryFile": "src/public-api.ts"
}
}
import { describe, it, expect, beforeEach } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import type { AstNode } from 'flatmarkdown-ast';
import { FmNodeComponent } from './fm-node.component';
import { defaultContext, RenderContext } from './types';
describe('FmNodeComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FmNodeComponent],
providers: [provideRouter([])],
});
});
function render(node: AstNode, ctx?: RenderContext): HTMLElement {
const fixture = TestBed.createComponent(FmNodeComponent);
fixture.componentRef.setInput('node', node);
fixture.componentRef.setInput('context', ctx ?? defaultContext());
fixture.detectChanges();
return fixture.nativeElement;
}
// ── Block Nodes ──
describe('document', () => {
it('renders children', () => {
const el = render({
type: 'document',
children: [
{ type: 'paragraph', children: [{ type: 'text', value: 'hello' }] },
],
});
expect(el.querySelector('p')!.textContent).toContain('hello');
});
});
describe('paragraph', () => {
it('wraps in <p>', () => {
const el = render({
type: 'paragraph',
children: [{ type: 'text', value: 'test' }],
});
expect(el.querySelector('p')).toBeTruthy();
expect(el.querySelector('p')!.textContent).toContain('test');
});
it('omits <p> in tight list', () => {
const el = render(
{ type: 'paragraph', children: [{ type: 'text', value: 'tight' }] },
{ ...defaultContext(), tightList: true },
);
expect(el.querySelector('p')).toBeNull();
expect(el.textContent).toContain('tight');
});
});
describe('heading', () => {
for (let level = 1; level <= 6; level++) {
it(`renders <h${level}>`, () => {
const el = render({
type: 'heading',
level,
setext: false,
children: [{ type: 'text', value: `Heading ${level}` }],
});
const h = el.querySelector(`h${level}`);
expect(h).toBeTruthy();
expect(h!.textContent).toContain(`Heading ${level}`);
});
}
});
describe('code_block', () => {
it('renders <pre><code>', () => {
const el = render({ type: 'code_block', fenced: true, info: '', literal: 'let x = 1;' });
const code = el.querySelector('pre > code');
expect(code).toBeTruthy();
expect(code!.textContent).toBe('let x = 1;');
});
it('adds language class from info', () => {
const el = render({ type: 'code_block', fenced: true, info: 'typescript', literal: 'const x = 1;' });
const code = el.querySelector('code');
expect(code!.className).toBe('language-typescript');
});
it('no class when info is empty', () => {
const el = render({ type: 'code_block', fenced: true, info: '', literal: 'x' });
const code = el.querySelector('code');
expect(code!.className).toBe('');
});
});
describe('block_quote', () => {
it('renders <blockquote>', () => {
const el = render({
type: 'block_quote',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'quoted' }] }],
});
expect(el.querySelector('blockquote')).toBeTruthy();
expect(el.querySelector('blockquote p')!.textContent).toContain('quoted');
});
});
describe('multiline_block_quote', () => {
it('renders <blockquote>', () => {
const el = render({
type: 'multiline_block_quote',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'multi' }] }],
});
expect(el.querySelector('blockquote')).toBeTruthy();
});
});
describe('list', () => {
it('renders <ul> for unordered', () => {
const el = render({
type: 'list',
list_type: 'bullet',
start: null,
tight: false,
delimiter: '',
children: [
{ type: 'item', list_type: 'bullet', start: 0, tight: false, children: [{ type: 'paragraph', children: [{ type: 'text', value: 'item1' }] }] },
],
});
expect(el.querySelector('ul')).toBeTruthy();
expect(el.querySelector('li')).toBeTruthy();
});
it('renders <ol> for ordered', () => {
const el = render({
type: 'list',
list_type: 'ordered',
start: 1,
tight: false,
delimiter: '.',
children: [
{ type: 'item', list_type: 'ordered', start: 1, tight: false, children: [{ type: 'paragraph', children: [{ type: 'text', value: 'first' }] }] },
],
});
expect(el.querySelector('ol')).toBeTruthy();
});
it('sets start attribute when not 1', () => {
const el = render({
type: 'list',
list_type: 'ordered',
start: 3,
tight: false,
delimiter: '.',
children: [
{ type: 'item', list_type: 'ordered', start: 3, tight: false, children: [{ type: 'paragraph', children: [{ type: 'text', value: 'third' }] }] },
],
});
expect(el.querySelector('ol')!.getAttribute('start')).toBe('3');
});
});
describe('item', () => {
it('renders <li> and passes tight context', () => {
const el = render({
type: 'item',
list_type: 'bullet',
start: 0,
tight: true,
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'tight item' }] }],
});
const li = el.querySelector('li');
expect(li).toBeTruthy();
// tight=true: paragraph should NOT be wrapped in <p>
expect(li!.querySelector('p')).toBeNull();
expect(li!.textContent).toContain('tight item');
});
});
describe('task_item', () => {
it('renders unchecked checkbox', () => {
const el = render({
type: 'task_item',
symbol: null,
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'todo' }] }],
});
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement;
expect(checkbox).toBeTruthy();
expect(checkbox.checked).toBe(false);
expect(checkbox.disabled).toBe(true);
});
it('renders checked checkbox', () => {
const el = render({
type: 'task_item',
symbol: 'x',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'done' }] }],
});
const checkbox = el.querySelector('input[type="checkbox"]') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
});
});
describe('table', () => {
const tableNode: AstNode = {
type: 'table',
alignments: ['left', 'center', 'none'],
num_columns: 3,
num_rows: 2,
children: [
{
type: 'table_row',
header: true,
children: [
{ type: 'table_cell', children: [{ type: 'text', value: 'H1' }] },
{ type: 'table_cell', children: [{ type: 'text', value: 'H2' }] },
{ type: 'table_cell', children: [{ type: 'text', value: 'H3' }] },
],
},
{
type: 'table_row',
header: false,
children: [
{ type: 'table_cell', children: [{ type: 'text', value: 'A' }] },
{ type: 'table_cell', children: [{ type: 'text', value: 'B' }] },
{ type: 'table_cell', children: [{ type: 'text', value: 'C' }] },
],
},
],
};
it('renders <table>', () => {
const el = render(tableNode);
expect(el.querySelector('table')).toBeTruthy();
});
it('header row uses <th>', () => {
const el = render(tableNode);
const ths = el.querySelectorAll('th');
expect(ths.length).toBe(3);
expect(ths[0].textContent).toContain('H1');
});
it('body row uses <td>', () => {
const el = render(tableNode);
const tds = el.querySelectorAll('td');
expect(tds.length).toBe(3);
expect(tds[0].textContent).toContain('A');
});
it('applies alignment', () => {
const el = render(tableNode);
const ths = el.querySelectorAll('th');
expect(ths[0].getAttribute('align')).toBe('left');
expect(ths[1].getAttribute('align')).toBe('center');
expect(ths[2].getAttribute('align')).toBeNull(); // 'none' → no align
});
});
describe('thematic_break', () => {
it('renders <hr>', () => {
const el = render({ type: 'thematic_break' });
expect(el.querySelector('hr')).toBeTruthy();
});
});
describe('html_block', () => {
it('passes through HTML via innerHTML', () => {
const el = render({ type: 'html_block', block_type: 6, literal: '<div class="custom">hi</div>' });
expect(el.querySelector('.custom')).toBeTruthy();
});
});
describe('footnote_definition', () => {
it('renders div with id', () => {
const el = render({
type: 'footnote_definition',
name: 'note1',
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'footnote text' }] }],
});
const div = el.querySelector('div.footnote');
expect(div).toBeTruthy();
expect(div!.id).toBe('fn-note1');
});
});
describe('description_list', () => {
it('renders dl/dt/dd', () => {
const el = render({
type: 'description_list',
children: [
{
type: 'description_item',
children: [
{ type: 'description_term', children: [{ type: 'text', value: 'Term' }] },
{ type: 'description_details', children: [{ type: 'text', value: 'Details' }] },
],
},
],
});
expect(el.querySelector('dl')).toBeTruthy();
expect(el.querySelector('dt')!.textContent).toContain('Term');
expect(el.querySelector('dd')!.textContent).toContain('Details');
});
});
describe('alert', () => {
it('renders div with alert class', () => {
const el = render({
type: 'alert',
alert_type: 'warning',
title: null,
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Careful!' }] }],
});
const div = el.querySelector('div.alert.alert-warning');
expect(div).toBeTruthy();
});
});
describe('front_matter', () => {
it('renders nothing', () => {
const el = render({ type: 'front_matter', value: 'title: hello' });
expect(el.textContent!.trim()).toBe('');
});
});
describe('heex_block', () => {
it('renders children', () => {
const el = render({
type: 'heex_block',
children: [{ type: 'text', value: 'heex' }],
});
expect(el.textContent).toContain('heex');
});
});
describe('subtext', () => {
it('renders children', () => {
const el = render({
type: 'subtext',
children: [{ type: 'text', value: 'sub' }],
});
expect(el.textContent).toContain('sub');
});
});
// ── Inline Nodes ──
describe('text', () => {
it('renders value', () => {
const el = render({ type: 'text', value: 'hello world' });
expect(el.textContent).toContain('hello world');
});
});
describe('softbreak', () => {
it('renders newline', () => {
const el = render({ type: 'softbreak' });
expect(el.textContent).toContain('\n');
});
});
describe('linebreak', () => {
it('renders <br>', () => {
const el = render({ type: 'linebreak' });
expect(el.querySelector('br')).toBeTruthy();
});
});
describe('emph', () => {
it('renders <em>', () => {
const el = render({
type: 'emph',
children: [{ type: 'text', value: 'italic' }],
});
const em = el.querySelector('em');
expect(em).toBeTruthy();
expect(em!.textContent).toContain('italic');
});
});
describe('strong', () => {
it('renders <strong>', () => {
const el = render({
type: 'strong',
children: [{ type: 'text', value: 'bold' }],
});
expect(el.querySelector('strong')!.textContent).toContain('bold');
});
});
describe('strikethrough', () => {
it('renders <del>', () => {
const el = render({
type: 'strikethrough',
children: [{ type: 'text', value: 'deleted' }],
});
expect(el.querySelector('del')!.textContent).toContain('deleted');
});
});
describe('underline', () => {
it('renders <u>', () => {
const el = render({
type: 'underline',
children: [{ type: 'text', value: 'underlined' }],
});
expect(el.querySelector('u')!.textContent).toContain('underlined');
});
});
describe('highlight', () => {
it('renders <mark>', () => {
const el = render({
type: 'highlight',
children: [{ type: 'text', value: 'highlighted' }],
});
expect(el.querySelector('mark')!.textContent).toContain('highlighted');
});
});
describe('superscript', () => {
it('renders <sup>', () => {
const el = render({
type: 'superscript',
children: [{ type: 'text', value: '2' }],
});
expect(el.querySelector('sup')!.textContent).toContain('2');
});
});
describe('subscript', () => {
it('renders <sub>', () => {
const el = render({
type: 'subscript',
children: [{ type: 'text', value: '2' }],
});
expect(el.querySelector('sub')!.textContent).toContain('2');
});
});
describe('spoilered_text', () => {
it('renders <span class="spoiler">', () => {
const el = render({
type: 'spoilered_text',
children: [{ type: 'text', value: 'secret' }],
});
const span = el.querySelector('span.spoiler');
expect(span).toBeTruthy();
expect(span!.textContent).toContain('secret');
});
});
describe('code', () => {
it('renders <code>', () => {
const el = render({ type: 'code', literal: 'x++' });
const code = el.querySelector('code');
expect(code).toBeTruthy();
expect(code!.textContent).toBe('x++');
});
});
describe('link', () => {
it('renders <a> with href', () => {
const el = render({
type: 'link',
url: 'https://example.com',
title: 'Example',
children: [{ type: 'text', value: 'click' }],
});
const a = el.querySelector('a');
expect(a).toBeTruthy();
expect(a!.getAttribute('href')).toBe('https://example.com');
expect(a!.getAttribute('title')).toBe('Example');
expect(a!.textContent).toContain('click');
});
it('omits title when empty', () => {
const el = render({
type: 'link',
url: 'https://example.com',
title: '',
children: [{ type: 'text', value: 'link' }],
});
expect(el.querySelector('a')!.getAttribute('title')).toBeNull();
});
});
describe('image', () => {
it('renders <img> with src and alt', () => {
const el = render({
type: 'image',
url: 'pic.png',
title: 'Photo',
children: [{ type: 'text', value: 'alt text' }],
});
const img = el.querySelector('img');
expect(img).toBeTruthy();
expect(img!.getAttribute('src')).toBe('pic.png');
expect(img!.getAttribute('alt')).toBe('alt text');
expect(img!.getAttribute('title')).toBe('Photo');
});
});
describe('wikilink', () => {
it('renders <a> with data-wikilink', () => {
const el = render({
type: 'wikilink',
url: 'MyPage',
children: [{ type: 'text', value: 'My Page' }],
});
const a = el.querySelector('a[data-wikilink]');
expect(a).toBeTruthy();
expect(a!.textContent).toContain('My Page');
});
it('uses routerLinkPrefix from options', () => {
const ctx = defaultContext({ wikilink: { routerLinkPrefix: '/wiki/' } });
const el = render(
{ type: 'wikilink', url: 'MyPage', children: [{ type: 'text', value: 'link' }] },
ctx,
);
const a = el.querySelector('a[data-wikilink]');
expect(a!.getAttribute('href')).toBe('/wiki/MyPage');
});
it('default prefix is empty string', () => {
const el = render({
type: 'wikilink',
url: 'MyPage',
children: [{ type: 'text', value: 'link' }],
});
const a = el.querySelector('a[data-wikilink]');
expect(a!.getAttribute('href')).toBe('/MyPage');
});
it('applies cssClass', () => {
const ctx = defaultContext({ wikilink: { cssClass: 'wiki-link' } });
const el = render(
{ type: 'wikilink', url: 'Page', children: [{ type: 'text', value: 'P' }] },
ctx,
);
const a = el.querySelector('a[data-wikilink]');
expect(a!.classList.contains('wiki-link')).toBe(true);
});
});
describe('hashtag', () => {
it('renders <a> with data-hashtag', () => {
const el = render({
type: 'hashtag',
value: 'tag1',
});
const a = el.querySelector('a[data-hashtag]');
expect(a).toBeTruthy();
expect(a!.textContent).toContain('tag1');
});
it('uses routerLinkPrefix from hashtag options', () => {
const ctx = defaultContext({ hashtag: { routerLinkPrefix: '/tags/' } });
const el = render(
{ type: 'hashtag', value: 'mytag' },
ctx,
);
const a = el.querySelector('a[data-hashtag]');
expect(a!.getAttribute('href')).toBe('/tags/mytag');
});
it('default prefix is empty string', () => {
const el = render({
type: 'hashtag',
value: 'mytag',
});
const a = el.querySelector('a[data-hashtag]');
expect(a!.getAttribute('href')).toBe('/mytag');
});
it('applies cssClass from hashtag options', () => {
const ctx = defaultContext({ hashtag: { cssClass: 'hashtag-link' } });
const el = render(
{ type: 'hashtag', value: 'tag' },
ctx,
);
const a = el.querySelector('a[data-hashtag]');
expect(a!.classList.contains('hashtag-link')).toBe(true);
});
});
describe('footnote_reference', () => {
it('renders superscript link', () => {
const el = render({ type: 'footnote_reference', name: 'fn1', ref_num: 1, ix: 0 });
const a = el.querySelector('sup > a');
expect(a).toBeTruthy();
expect(a!.getAttribute('href')).toBe('#fn-fn1');
expect(a!.textContent).toContain('[1]');
});
it('uses ix+1 for display number', () => {
const el = render({ type: 'footnote_reference', name: 'fn2', ref_num: 2, ix: 4 });
expect(el.querySelector('sup > a')!.textContent).toContain('[5]');
});
});
describe('shortcode', () => {
it('renders emoji', () => {
const el = render({ type: 'shortcode', code: 'smile', emoji: '😊' });
expect(el.textContent).toContain('😊');
});
});
describe('math', () => {
it('renders display math in <pre><code>', () => {
const el = render({ type: 'math', dollar_math: true, display_math: true, literal: 'E=mc^2' });
const code = el.querySelector('pre > code.math.math-display');
expect(code).toBeTruthy();
expect(code!.textContent).toBe('E=mc^2');
});
it('renders inline math in <code>', () => {
const el = render({ type: 'math', dollar_math: true, display_math: false, literal: 'x^2' });
const code = el.querySelector('code.math.math-inline');
expect(code).toBeTruthy();
expect(code!.textContent).toBe('x^2');
expect(el.querySelector('pre')).toBeNull();
});
});
describe('html_inline', () => {
it('passes through HTML via innerHTML', () => {
const el = render({ type: 'html_inline', value: '<b>bold</b>' });
expect(el.querySelector('b')!.textContent).toBe('bold');
});
});
describe('heex_inline', () => {
it('passes through via innerHTML', () => {
const el = render({ type: 'heex_inline', value: '<i>italic</i>' });
expect(el.querySelector('i')).toBeTruthy();
});
});
describe('raw', () => {
it('passes through via innerHTML', () => {
const el = render({ type: 'raw', value: '<span>raw</span>' });
expect(el.querySelector('span')!.textContent).toBe('raw');
});
});
describe('escaped', () => {
it('renders nothing', () => {
const el = render({ type: 'escaped' });
expect(el.textContent!.trim()).toBe('');
});
});
describe('escaped_tag', () => {
it('renders escaped value as text', () => {
const el = render({ type: 'escaped_tag', value: '<div>' });
expect(el.textContent).toContain('<div>');
expect(el.querySelector('div')).toBeNull();
});
});
});
import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { RouterLink } from '@angular/router';
import type { AstNode } from 'flatmarkdown-ast';
import { RenderContext } from './types';
import { extractText } from './utils';
@Component({
selector: 'fm-node',
standalone: true,
imports: [NgTemplateOutlet, RouterLink],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { style: 'display:contents' },
template: `
@switch (node().type) {
<!-- ── Block Nodes ── -->
@case ('document') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('paragraph') {
@if (context().tightList) {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
} @else {
<p>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</p>
}
}
@case ('heading') {
<ng-template #headingContent>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ng-template>
@switch ($any(node()).level) {
@case (1) { <h1><ng-container *ngTemplateOutlet="headingContent" /></h1> }
@case (2) { <h2><ng-container *ngTemplateOutlet="headingContent" /></h2> }
@case (3) { <h3><ng-container *ngTemplateOutlet="headingContent" /></h3> }
@case (4) { <h4><ng-container *ngTemplateOutlet="headingContent" /></h4> }
@case (5) { <h5><ng-container *ngTemplateOutlet="headingContent" /></h5> }
@case (6) { <h6><ng-container *ngTemplateOutlet="headingContent" /></h6> }
}
}
@case ('code_block') {
<pre><code [class]="$any(node()).info ? 'language-' + $any(node()).info : null">{{ $any(node()).literal }}</code></pre>
}
@case ('block_quote') {
<blockquote>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</blockquote>
}
@case ('multiline_block_quote') {
<blockquote>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</blockquote>
}
@case ('list') {
@if ($any(node()).list_type === 'ordered') {
<ol [attr.start]="$any(node()).start != null && $any(node()).start !== 1 ? $any(node()).start : null">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ol>
} @else {
<ul>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</ul>
}
}
@case ('item') {
<li>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tightList: $any(node()).tight })" />
}
</li>
}
@case ('task_item') {
<li>
<input type="checkbox" [checked]="$any(node()).symbol != null" disabled />
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tightList: true })" />
}
</li>
}
@case ('table') {
<table>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ tableAlignments: $any(node()).alignments })" />
}
</table>
}
@case ('table_row') {
<tr>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="childContext({ inHeaderRow: $any(node()).header, cellIndex: $index })" />
}
</tr>
}
@case ('table_cell') {
@if (context().inHeaderRow) {
<th [attr.align]="cellAlignment()">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</th>
} @else {
<td [attr.align]="cellAlignment()">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</td>
}
}
@case ('thematic_break') {
<hr />
}
@case ('html_block') {
<span [innerHTML]="$any(node()).literal"></span>
}
@case ('footnote_definition') {
<div class="footnote" [id]="'fn-' + $any(node()).name">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</div>
}
@case ('description_list') {
<dl>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dl>
}
@case ('description_item') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('description_term') {
<dt>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dt>
}
@case ('description_details') {
<dd>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</dd>
}
@case ('alert') {
<div [class]="'alert alert-' + $any(node()).alert_type">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</div>
}
@case ('front_matter') {
<!-- front_matter is not rendered -->
}
@case ('heex_block') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
@case ('subtext') {
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
}
<!-- ── Inline Nodes ── -->
@case ('text') {
{{ $any(node()).value }}
}
@case ('softbreak') {
{{ '\n' }}
}
@case ('linebreak') {
<br />
}
@case ('emph') {
<em>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</em>
}
@case ('strong') {
<strong>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</strong>
}
@case ('strikethrough') {
<del>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</del>
}
@case ('underline') {
<u>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</u>
}
@case ('highlight') {
<mark>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</mark>
}
@case ('superscript') {
<sup>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</sup>
}
@case ('subscript') {
<sub>
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</sub>
}
@case ('spoilered_text') {
<span class="spoiler">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</span>
}
@case ('code') {
<code>{{ $any(node()).literal }}</code>
}
@case ('link') {
<a [href]="$any(node()).url" [attr.title]="$any(node()).title || null">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</a>
}
@case ('image') {
<img [src]="$any(node()).url" [alt]="altText()" [attr.title]="$any(node()).title || null" />
}
@case ('wikilink') {
<a [routerLink]="wikilinkRoute()"
[class]="context().options.wikilink?.cssClass || null"
data-wikilink="true">
@for (child of children(); track $index) {
<fm-node [node]="child" [context]="context()" />
}
</a>
}
@case ('hashtag') {
<a [routerLink]="hashtagRoute()"
[class]="context().options.hashtag?.cssClass || null"
data-hashtag="true">{{ $any(node()).value }}</a>
}
@case ('footnote_reference') {
<sup><a [href]="'#fn-' + $any(node()).name">{{ '[' + ($any(node()).ix + 1) + ']' }}</a></sup>
}
@case ('shortcode') {
{{ $any(node()).emoji }}
}
@case ('math') {
@if ($any(node()).display_math) {
<pre><code class="math math-display">{{ $any(node()).literal }}</code></pre>
} @else {
<code class="math math-inline">{{ $any(node()).literal }}</code>
}
}
@case ('html_inline') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('heex_inline') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('raw') {
<span [innerHTML]="$any(node()).value"></span>
}
@case ('escaped') {
<!-- escaped produces no output -->
}
@case ('escaped_tag') {
{{ $any(node()).value }}
}
}
`,
})
export class FmNodeComponent {
readonly node = input.required<AstNode>();
readonly context = input.required<RenderContext>();
readonly children = computed(() => {
const n = this.node();
return 'children' in n && n.children ? n.children : [];
});
readonly cellAlignment = computed(() => {
const ctx = this.context();
const alignment = ctx.tableAlignments[ctx.cellIndex];
return alignment && alignment !== 'none' ? alignment : null;
});
readonly altText = computed(() => extractText(this.node()));
readonly wikilinkRoute = computed(() => {
const n = this.node() as { url: string };
const prefix = this.context().options.wikilink?.routerLinkPrefix ?? '';
return prefix + n.url;
});
readonly hashtagRoute = computed(() => {
const n = this.node() as { value: string };
const prefix = this.context().options.hashtag?.routerLinkPrefix ?? '';
return prefix + n.value;
});
childContext(overrides: Partial<RenderContext>): RenderContext {
return { ...this.context(), ...overrides };
}
}
import { describe, it, expect, beforeEach } from 'vitest';
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import type { AstNode } from 'flatmarkdown-ast';
import { FmRootComponent } from './fm-root.component';
describe('FmRootComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FmRootComponent],
providers: [provideRouter([])],
});
});
it('renders AST object', () => {
const fixture = TestBed.createComponent(FmRootComponent);
const ast: AstNode = {
type: 'document',
children: [
{ type: 'paragraph', children: [{ type: 'text', value: 'Hello' }] },
],
};
fixture.componentRef.setInput('ast', ast);
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('p')!.textContent).toContain('Hello');
});
it('parses JSON string input', () => {
const fixture = TestBed.createComponent(FmRootComponent);
const ast: AstNode = {
type: 'document',
children: [
{ type: 'heading', level: 1, setext: false, children: [{ type: 'text', value: 'Title' }] },
],
};
fixture.componentRef.setInput('ast', JSON.stringify(ast));
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('h1')!.textContent).toContain('Title');
});
it('passes options to context', () => {
const fixture = TestBed.createComponent(FmRootComponent);
const ast: AstNode = {
type: 'document',
children: [
{ type: 'wikilink', url: 'Page', children: [{ type: 'text', value: 'Link' }] },
],
};
fixture.componentRef.setInput('ast', ast);
fixture.componentRef.setInput('options', {
wikilink: { routerLinkPrefix: '/docs/', cssClass: 'my-link' },
});
fixture.detectChanges();
const a = fixture.nativeElement.querySelector('a[data-wikilink]');
expect(a.getAttribute('href')).toBe('/docs/Page');
expect(a.classList.contains('my-link')).toBe(true);
});
});
import { Component, ChangeDetectionStrategy, input, computed } from '@angular/core';
import type { AstNode } from 'flatmarkdown-ast';
import { AngularRenderOptions, RenderContext, defaultContext } from './types';
import { FmNodeComponent } from './fm-node.component';
@Component({
selector: 'fm-root',
standalone: true,
imports: [FmNodeComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { style: 'display:contents' },
template: `
@if (rootNode(); as root) {
<fm-node [node]="root" [context]="rootContext()" />
}
`,
})
export class FmRootComponent {
readonly ast = input.required<AstNode | string>();
readonly options = input<AngularRenderOptions>({});
readonly rootNode = computed<AstNode | null>(() => {
const v = this.ast();
if (!v) return null;
return typeof v === 'string' ? JSON.parse(v) : v;
});
readonly rootContext = computed<RenderContext>(() => defaultContext(this.options()));
}
import { describe, it, expect } from 'vitest';
import { defaultContext } from './types';
describe('defaultContext', () => {
it('returns default values', () => {
const ctx = defaultContext();
expect(ctx.options).toEqual({});
expect(ctx.inHeaderRow).toBe(false);
expect(ctx.tableAlignments).toEqual([]);
expect(ctx.cellIndex).toBe(0);
expect(ctx.tightList).toBe(false);
});
it('stores provided options', () => {
const options = { wikilink: { routerLinkPrefix: '/wiki/' } };
const ctx = defaultContext(options);
expect(ctx.options).toBe(options);
});
});
import type { AstNode } from 'flatmarkdown-ast';
export interface WikiLinkAngularOptions {
/** Prefix prepended to the wikilink url for routerLink. Default: '' */
routerLinkPrefix?: string;
/** CSS class applied to wikilink anchors. */
cssClass?: string;
}
export interface HashtagAngularOptions {
/** Prefix prepended to the hashtag value for routerLink. Default: '' */
routerLinkPrefix?: string;
/** CSS class applied to hashtag anchors. */
cssClass?: string;
}
export interface AngularRenderOptions {
wikilink?: WikiLinkAngularOptions;
hashtag?: HashtagAngularOptions;
}
export interface RenderContext {
options: AngularRenderOptions;
inHeaderRow: boolean;
tableAlignments: string[];
cellIndex: number;
tightList: boolean;
}
export function defaultContext(options: AngularRenderOptions = {}): RenderContext {
return {
options,
inHeaderRow: false,
tableAlignments: [],
cellIndex: 0,
tightList: false,
};
}
import { describe, it, expect } from 'vitest';
import type { AstNode } from 'flatmarkdown-ast';
import { extractText } from './utils';
describe('extractText', () => {
it('returns value from text node', () => {
expect(extractText({ type: 'text', value: 'hello' })).toBe('hello');
});
it('concatenates text from nested children', () => {
const node: AstNode = {
type: 'emph',
children: [
{ type: 'text', value: 'foo' },
{ type: 'strong', children: [{ type: 'text', value: 'bar' }] },
],
};
expect(extractText(node)).toBe('foobar');
});
it('returns empty string for nodes without text', () => {
expect(extractText({ type: 'linebreak' })).toBe('');
});
it('returns empty string for node with no children', () => {
expect(extractText({ type: 'emph' })).toBe('');
});
});
import type { AstNode } from 'flatmarkdown-ast';
export function extractText(node: AstNode): string {
if (node.type === 'text') return node.value;
if ('children' in node && node.children) {
return node.children.map(extractText).join('');
}
return '';
}
export { FmRootComponent } from './lib/fm-root.component';
export { FmNodeComponent } from './lib/fm-node.component';
export type { AngularRenderOptions, WikiLinkAngularOptions, RenderContext } from './lib/types';
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "DOM"],
"moduleResolution": "bundler",
"strict": true,
"declaration": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": false,
"experimentalDecorators": true
}
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/lib",
"declarationMap": true,
"sourceMap": true
},
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"files": ["src/public-api.ts"],
"include": ["src/**/*.ts"]
}
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false,
"sourceMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": []
},
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"include": ["src/**/*.spec.ts", "src/**/*.ts"]
}
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config';
import angular from '@analogjs/vite-plugin-angular';
export default defineConfig({
plugins: [angular()],
test: {
globals: true,
include: ['src/**/*.spec.ts'],
environment: 'jsdom',
setupFiles: ['src/setup-test.ts'],
},
});