import 'package:flutter/material.dart'; import '../../theming/app_theme.dart'; import 'pm_callout_view.dart'; import 'pm_image_view.dart'; import 'pm_node.dart'; import 'pm_render_scope.dart'; import 'pm_rich_text.dart'; import 'pm_table_view.dart'; /// Read-only renderer for a parsed ProseMirror document. /// /// Content is centered and width-capped for comfortable line lengths on /// tablets. All styling derives from `Theme.of(context)`; the only hard-coded /// colors are the callout accents. class PmDocumentView extends StatelessWidget { final PmNode doc; final void Function(String href)? onLinkTap; final double maxContentWidth; const PmDocumentView({ required this.doc, this.onLinkTap, this.maxContentWidth = 720, super.key, }); @override Widget build(BuildContext context) { return PmRenderScope( onLinkTap: onLinkTap, child: Center( child: ConstrainedBox( constraints: BoxConstraints(maxWidth: maxContentWidth), child: PmNodeView(node: doc), ), ), ); } } typedef PmNodeBuilder = Widget Function(PmNode node); /// Dispatches a node to its registered widget, falling back to /// [FallbackNodeWidget] for anything unregistered (including [PmUnknown]). class PmNodeView extends StatelessWidget { final PmNode node; const PmNodeView({required this.node, super.key}); static final Map registry = { PmParagraph: (n) => PmParagraphView(node: n as PmParagraph), PmHeading: (n) => PmHeadingView(node: n as PmHeading), PmBulletList: (n) => PmBulletListView(node: n as PmBulletList), PmOrderedList: (n) => PmOrderedListView(node: n as PmOrderedList), PmBlockquote: (n) => PmBlockquoteView(node: n as PmBlockquote), PmCodeBlock: (n) => PmCodeBlockView(node: n as PmCodeBlock), PmHorizontalRule: (n) => const PmHorizontalRuleView(), PmCallout: (n) => PmCalloutView(node: n as PmCallout), PmImage: (n) => PmImageView(node: n as PmImage), PmTable: (n) => PmTableView(node: n as PmTable), }; @override Widget build(BuildContext context) { final builder = registry[node.runtimeType]; if (builder != null) return builder(node); return FallbackNodeWidget(node: node); } } /// Renders a node's children as a vertical block stack. Used for the document /// root, unknown nodes, and any container whose type has no dedicated widget. class FallbackNodeWidget extends StatelessWidget { final PmNode node; const FallbackNodeWidget({required this.node, super.key}); @override Widget build(BuildContext context) { if (node.children.isEmpty) return const SizedBox.shrink(); return pmBlocks(node.children); } } /// Lays out block-level nodes in a column, evenly spaced. Widget pmBlocks(List nodes, {double gap = AppSpacing.sm}) { final children = []; for (var i = 0; i < nodes.length; i++) { if (i > 0) children.add(SizedBox(height: gap)); children.add(PmNodeView(node: nodes[i])); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: children, ); } class PmParagraphView extends StatelessWidget { final PmParagraph node; const PmParagraphView({required this.node, super.key}); @override Widget build(BuildContext context) { final base = Theme.of(context).textTheme.bodyMedium ?? const TextStyle(); return PmRichText( inlines: node.children, baseStyle: base, textAlign: node.align ?? TextAlign.start, ); } } class PmHeadingView extends StatelessWidget { final PmHeading node; const PmHeadingView({required this.node, super.key}); @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; final base = switch (node.level) { 1 => textTheme.headlineSmall, 2 => textTheme.titleLarge, 3 => textTheme.titleMedium, _ => textTheme.titleSmall, }; return PmRichText( inlines: node.children, baseStyle: base ?? const TextStyle(), textAlign: node.align ?? TextAlign.start, ); } } class PmBulletListView extends StatelessWidget { final PmBulletList node; const PmBulletListView({required this.node, super.key}); @override Widget build(BuildContext context) => _ListLayout(items: node.children, markerFor: (_) => '•'); } class PmOrderedListView extends StatelessWidget { final PmOrderedList node; const PmOrderedListView({required this.node, super.key}); @override Widget build(BuildContext context) => _ListLayout( items: node.children, markerFor: (index) => '${node.start + index}.', ); } class _ListLayout extends StatelessWidget { final List items; final String Function(int index) markerFor; const _ListLayout({required this.items, required this.markerFor}); @override Widget build(BuildContext context) { final base = Theme.of(context).textTheme.bodyMedium ?? const TextStyle(); final rows = []; for (var i = 0; i < items.length; i++) { if (i > 0) rows.add(const SizedBox(height: AppSpacing.xs)); rows.add( Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 24, child: Text( markerFor(i), textAlign: TextAlign.right, style: base, ), ), const SizedBox(width: AppSpacing.sm), Expanded(child: pmBlocks(items[i].children, gap: AppSpacing.xs)), ], ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: rows, ); } } class PmBlockquoteView extends StatelessWidget { final PmBlockquote node; const PmBlockquoteView({required this.node, super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( padding: const EdgeInsets.only(left: AppSpacing.md), decoration: BoxDecoration( border: Border( left: BorderSide(color: theme.colorScheme.outlineVariant, width: 4), ), ), child: pmBlocks(node.children), ); } } class PmCodeBlockView extends StatelessWidget { final PmCodeBlock node; const PmCodeBlockView({required this.node, super.key}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final code = node.children.whereType().map((t) => t.text).join(); return DecoratedBox( decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Padding( padding: const EdgeInsets.all(AppSpacing.md), child: Text( code, style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), ), ), ), ); } } class PmHorizontalRuleView extends StatelessWidget { const PmHorizontalRuleView({super.key}); @override Widget build(BuildContext context) => const Divider(); }