flatmarkdown-ast2angular
Advanced tools
| 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":";;;;;AA6BM,SAAU,cAAc,CAAC,OAAA,GAAgC,EAAE,EAAA;IAC/D,OAAO;QACL,OAAO;AACP,QAAA,WAAW,EAAE,KAAK;AAClB,QAAA,eAAe,EAAE,EAAE;AACnB,QAAA,SAAS,EAAE,CAAC;AACZ,QAAA,SAAS,EAAE,KAAK;KACjB;AACH;;ACnCM,SAAU,WAAW,CAAC,IAAa,EAAA;AACvC,IAAA,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,KAAK;IAC3C,IAAI,UAAU,IAAI,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE;AACvC,QAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;IAChD;AACA,IAAA,OAAO,EAAE;AACX;;MC4Va,eAAe,CAAA;AACjB,IAAA,IAAI,GAAG,KAAK,CAAC,QAAQ,+CAAW;AAChC,IAAA,OAAO,GAAG,KAAK,CAAC,QAAQ,kDAAiB;AAEzC,IAAA,QAAQ,GAAG,QAAQ,CAAC,MAAK;AAChC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE;AACrB,QAAA,OAAO,UAAU,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,GAAG,EAAE;AACxD,IAAA,CAAC,oDAAC;AAEO,IAAA,aAAa,GAAG,QAAQ,CAAC,MAAK;AACrC,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE;QAC1B,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC;AACpD,QAAA,OAAO,SAAS,IAAI,SAAS,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI;AAC7D,IAAA,CAAC,yDAAC;AAEO,IAAA,OAAO,GAAG,QAAQ,CAAC,MAAM,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,mDAAC;AAElD,IAAA,aAAa,GAAG,QAAQ,CAAC,MAAK;AACrC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAqB;AACxC,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,gBAAgB,IAAI,EAAE;AACtE,QAAA,OAAO,MAAM,GAAG,CAAC,CAAC,GAAG;AACvB,IAAA,CAAC,yDAAC;AAEO,IAAA,YAAY,GAAG,QAAQ,CAAC,MAAK;AACpC,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAuB;AAC1C,QAAA,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,gBAAgB,IAAI,EAAE;AACrE,QAAA,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK;AACzB,IAAA,CAAC,wDAAC;AAEF,IAAA,YAAY,CAAC,SAAiC,EAAA;QAC5C,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,GAAG,SAAS,EAAE;IAC5C;uGA/BW,eAAe,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAf,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,eAAe,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,SAAA,EAAA,MAAA,EAAA,EAAA,IAAA,EAAA,EAAA,iBAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,cAAA,EAAA,kBAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EAvVhB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqVT,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAEU,eAAe,EAAA,QAAA,EAAA,SAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,SAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EA1VhB,gBAAgB,EAAA,QAAA,EAAA,oBAAA,EAAA,MAAA,EAAA,CAAA,yBAAA,EAAA,kBAAA,EAAA,0BAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAE,UAAU,EAAA,QAAA,EAAA,cAAA,EAAA,MAAA,EAAA,CAAA,QAAA,EAAA,aAAA,EAAA,UAAA,EAAA,qBAAA,EAAA,OAAA,EAAA,MAAA,EAAA,YAAA,EAAA,kBAAA,EAAA,oBAAA,EAAA,YAAA,EAAA,YAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FA0V3B,eAAe,EAAA,UAAA,EAAA,CAAA;kBA7V3B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,SAAS;AACnB,oBAAA,UAAU,EAAE,IAAI;AAChB,oBAAA,OAAO,EAAE,CAAC,gBAAgB,EAAE,UAAU,CAAC;oBACvC,eAAe,EAAE,uBAAuB,CAAC,MAAM;AAC/C,oBAAA,IAAI,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE;AACnC,oBAAA,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqVT,EAAA,CAAA;AACF,iBAAA;;;MClVY,eAAe,CAAA;AACjB,IAAA,GAAG,GAAG,KAAK,CAAC,QAAQ,8CAAoB;AACxC,IAAA,OAAO,GAAG,KAAK,CAAuB,EAAE,mDAAC;AAEzC,IAAA,QAAQ,GAAG,QAAQ,CAAiB,MAAK;AAChD,QAAA,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;AACpB,QAAA,IAAI,CAAC,CAAC;AAAE,YAAA,OAAO,IAAI;AACnB,QAAA,OAAO,OAAO,CAAC,KAAK,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC;AAClD,IAAA,CAAC,oDAAC;AAEO,IAAA,WAAW,GAAG,QAAQ,CAAgB,MAAM,cAAc,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,uDAAC;uGAVzE,eAAe,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAf,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,eAAe,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,SAAA,EAAA,MAAA,EAAA,EAAA,GAAA,EAAA,EAAA,iBAAA,EAAA,KAAA,EAAA,UAAA,EAAA,KAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,cAAA,EAAA,kBAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EANhB;;;;AAIT,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAPS,eAAe,EAAA,QAAA,EAAA,SAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,SAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;2FASd,eAAe,EAAA,UAAA,EAAA,CAAA;kBAZ3B,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,SAAS;AACnB,oBAAA,UAAU,EAAE,IAAI;oBAChB,OAAO,EAAE,CAAC,eAAe,CAAC;oBAC1B,eAAe,EAAE,uBAAuB,CAAC,MAAM;AAC/C,oBAAA,IAAI,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE;AACnC,oBAAA,QAAQ,EAAE;;;;AAIT,EAAA,CAAA;AACF,iBAAA;;;AChBD;;AAEG;;;;"} |
| 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'], | ||
| }, | ||
| }); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
51031
16.73%0
-100%5
25%6
-68.42%735
-37.13%+ Added