diff --git a/changelog/v0.4.0.md b/changelog/v0.4.0.md index 2649559..6ef7f39 100644 --- a/changelog/v0.4.0.md +++ b/changelog/v0.4.0.md @@ -1,5 +1,10 @@ # v0.4.0 +## New Features +- XBRL data viewer: click any XBRL document to see formatted financial reports with tabbed navigation +- XBRL reports display human-readable names parsed from FilingSummary.xml +- XBRL badge on XBRL documents in the sidebar + ## Improvements - Refactored filing search into sub-components: result row, status bar, query item, query editor - Filing result row redesigned with CSS grid layout: ticker, title, description, tags, and date diff --git a/src/app/components/welcome-modal/changelog.ts b/src/app/components/welcome-modal/changelog.ts index ddb43eb..bdf09e0 100644 --- a/src/app/components/welcome-modal/changelog.ts +++ b/src/app/components/welcome-modal/changelog.ts @@ -9,6 +9,14 @@ export const CHANGELOG: ChangelogEntry[] = [ { version: '0.4.0', sections: [ + { + title: 'New Features', + items: [ + 'XBRL data viewer: click any XBRL document to see formatted financial reports with tabbed navigation', + 'XBRL reports display human-readable names parsed from FilingSummary.xml', + 'XBRL badge on XBRL documents in the sidebar', + ], + }, { title: 'Improvements', items: [ diff --git a/src/app/core/models/document.model.ts b/src/app/core/models/document.model.ts index 45a5592..899395a 100644 --- a/src/app/core/models/document.model.ts +++ b/src/app/core/models/document.model.ts @@ -101,3 +101,16 @@ export interface DocumentImageOptions { page?: number; } +/** + * A report entry parsed from FilingSummary.xml in XBRL filings. + */ +export interface XbrlReport { + shortName: string; + longName: string; + htmlFileName: string; + menuCategory: string; + position: number; + reportType: string; + role?: string; +} + diff --git a/src/app/core/services/document.service.ts b/src/app/core/services/document.service.ts index 8939c6f..33d3af4 100644 --- a/src/app/core/services/document.service.ts +++ b/src/app/core/services/document.service.ts @@ -138,6 +138,18 @@ export class DocumentService { return this.http.get(`${this.baseUrl}/${accessionNumber}/meta`); } + /** + * Get a document's content as text. + * Useful for fetching XML or HTML content for client-side parsing. + * @param accessionNumber - The document's accession number + * @param seq - The sequence number of the file + */ + getDocumentText(accessionNumber: string, seq: number | string): Observable { + return this.http.get(`${this.baseUrl}/${accessionNumber}/${seq}`, { + responseType: 'text', + }); + } + /** * Get context information for a document fragment. * @param accessionNumber - The document's accession number diff --git a/src/app/core/services/xbrl-parser.service.ts b/src/app/core/services/xbrl-parser.service.ts new file mode 100644 index 0000000..5755161 --- /dev/null +++ b/src/app/core/services/xbrl-parser.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { XbrlReport } from '../models/document.model'; + +/** + * Parses FilingSummary.xml from SEC XBRL filings + * into structured report metadata. + */ +@Injectable({ + providedIn: 'root', +}) +export class XbrlParserService { + /** + * Parse FilingSummary.xml content into an array of XBRL reports. + * Filters out the "All Reports" book entry and sorts by position. + */ + parseFilingSummary(xml: string): XbrlReport[] { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, 'text/xml'); + + const parseError = doc.querySelector('parsererror'); + if (parseError) { + console.error('Failed to parse FilingSummary.xml:', parseError.textContent); + return []; + } + + const reportElements = doc.querySelectorAll('MyReports > Report'); + const reports: XbrlReport[] = []; + + reportElements.forEach((el) => { + const reportType = this.getTextContent(el, 'ReportType'); + const htmlFileName = this.getTextContent(el, 'HtmlFileName'); + + // Skip the "All Reports" book entry and reports without HTML files + if (reportType === 'Book' || !htmlFileName) { + return; + } + + reports.push({ + shortName: this.getTextContent(el, 'ShortName') || htmlFileName, + longName: this.getTextContent(el, 'LongName') || '', + htmlFileName, + menuCategory: this.getTextContent(el, 'MenuCategory') || '', + position: parseInt(this.getTextContent(el, 'Position') || '0', 10), + reportType, + role: this.getTextContent(el, 'Role') || undefined, + }); + }); + + return reports.sort((a, b) => a.position - b.position); + } + + private getTextContent(parent: Element, tagName: string): string { + return parent.querySelector(tagName)?.textContent?.trim() || ''; + } +} diff --git a/src/app/features/document/document-viewer.component.css b/src/app/features/document/document-viewer.component.css index b886116..5f028e6 100644 --- a/src/app/features/document/document-viewer.component.css +++ b/src/app/features/document/document-viewer.component.css @@ -17,3 +17,140 @@ iframe { main { position: relative; } + +/* ============================================ + XBRL Report Viewer Styles + ============================================ + These target injected HTML from SEC R*.htm files. + Using ::ng-deep because [innerHTML] content is + outside Angular's view encapsulation. + ============================================ */ + +:host ::ng-deep .xbrl-content { + color: #cccccc; + font-size: 13px; + line-height: 1.5; +} + +:host ::ng-deep .xbrl-content table { + width: 100%; + border-collapse: collapse; + margin-bottom: 16px; + background: #1e1e1e; + border: 1px solid #3c3c3c; +} + +:host ::ng-deep .xbrl-content th { + background: #252526; + color: #cccccc; + font-weight: 600; + text-align: left; + padding: 8px 12px; + border: 1px solid #3c3c3c; + font-size: 12px; + white-space: nowrap; +} + +:host ::ng-deep .xbrl-content td { + padding: 6px 12px; + border: 1px solid #2d2d2d; + font-size: 12px; + vertical-align: top; +} + +:host ::ng-deep .xbrl-content tr:nth-child(even) { + background: #252526; +} + +:host ::ng-deep .xbrl-content tr:nth-child(odd) { + background: #1e1e1e; +} + +:host ::ng-deep .xbrl-content tr:hover { + background: #2a2d2e; +} + +/* Numeric values — right-align */ +:host ::ng-deep .xbrl-content td.nump, +:host ::ng-deep .xbrl-content td.num { + text-align: right; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; + color: #4ec9b0; +} + +/* Negative values */ +:host ::ng-deep .xbrl-content td.numn { + text-align: right; + font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; + color: #f14c4c; +} + +/* Row labels */ +:host ::ng-deep .xbrl-content td.pl, +:host ::ng-deep .xbrl-content td.text { + color: #cccccc; +} + +/* Bold/header rows within tables */ +:host ::ng-deep .xbrl-content tr.rh td, +:host ::ng-deep .xbrl-content td strong, +:host ::ng-deep .xbrl-content td b { + color: #ffffff; + font-weight: 600; +} + +/* Links in XBRL content */ +:host ::ng-deep .xbrl-content a { + color: #3794ff; + text-decoration: none; +} + +:host ::ng-deep .xbrl-content a:hover { + text-decoration: underline; +} + +/* Override any inline styles from SEC HTML */ +:host ::ng-deep .xbrl-content * { + background-color: transparent !important; + color: inherit; +} + +:host ::ng-deep .xbrl-content table, +:host ::ng-deep .xbrl-content th, +:host ::ng-deep .xbrl-content td { + background-color: revert !important; +} + +/* Report title styling */ +:host ::ng-deep .xbrl-content h1, +:host ::ng-deep .xbrl-content h2, +:host ::ng-deep .xbrl-content h3 { + color: #ffffff; + margin-bottom: 12px; +} + +/* Hide any embedded scripts or style tags */ +:host ::ng-deep .xbrl-content style, +:host ::ng-deep .xbrl-content script { + display: none; +} + +/* Scrollable table wrapper for wide tables */ +:host ::ng-deep .xbrl-content { + overflow-x: auto; +} + +/* Report tab scrollbar styling */ +.xbrl-content::-webkit-scrollbar { + height: 6px; + width: 6px; +} + +.xbrl-content::-webkit-scrollbar-thumb { + background: #424242; + border-radius: 3px; +} + +.xbrl-content::-webkit-scrollbar-track { + background: transparent; +} diff --git a/src/app/features/document/document-viewer.component.html b/src/app/features/document/document-viewer.component.html index ddbb670..2bd39a2 100644 --- a/src/app/features/document/document-viewer.component.html +++ b/src/app/features/document/document-viewer.component.html @@ -89,15 +89,20 @@
- {{ doc.filename }} + {{ getXbrlReportName(doc) || doc.filename }}
- @if (doc.description) { + @if (getXbrlReportName(doc)) { +
{{ doc.filename }}
+ } @else if (doc.description) {
{{ doc.description }}
}
@if (doc.sequence === 1) { PRIMARY } + @if (isXbrlDocument(doc)) { + XBRL + } } @@ -137,6 +142,40 @@

Failed to load filing

{{ error() }}

+ } @else if (xbrlMode()) { + +
+ +
+ @for (report of xbrlReports(); track report.position) { + + } +
+ + +
+ @if (xbrlLoading()) { +
+ +
+ } @else if (xbrlReportHtml()) { +
+ } @else { +
+ +

No XBRL reports found

+
+ } +
+
} @else if (iframeSrc()) {
@@ -146,7 +185,7 @@
} - +