Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
/// - Custom mark definitions
/// - Custom text mark recognizers
/// - Shared config across all Portable Text Widgets
/// - Per-subtree config overrides via [PortableTextTheme] / [PortableTextStyleOverride]
///
library;

export 'model/markdef_descriptor.dart';
export 'model/text_block.dart';
export 'ui/portable_text_block.dart';
export 'ui/portable_text_config.dart';
export 'ui/portable_text_theme.dart';
export 'ui/portable_text_widget.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ class Span {
_$SpanFromJson(json);
}

/// Resolves custom mark deserializers from [PortableTextConfig.shared].
///
/// This runs at JSON-parse time with no [BuildContext], so it must read the context-free
/// [PortableTextConfig.shared] registry rather than a per-subtree config. Mark *styling* is
/// still resolved per subtree at render time via `PortableTextConfig.of(context)`.
List<MarkDef> _markDefsFromJson(final List<dynamic> json) {
final markDefs = PortableTextConfig.shared.markDefs;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class PortableTextBlock extends StatelessWidget {

@override
Widget build(final BuildContext context) {
final config = PortableTextConfig.shared;
final config = PortableTextConfig.of(context);

final spans = model.children
.map((final span) => _buildInlineSpan(span, Theme.of(context), context))
Expand Down Expand Up @@ -52,7 +52,7 @@ class PortableTextBlock extends StatelessWidget {
final ThemeData theme,
final BuildContext context,
) {
final config = PortableTextConfig.shared;
final config = PortableTextConfig.of(context);

// Step 1: Start with the base style
final baseStyle = config.baseStyle(context) ??
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ typedef BulletRenderer = InlineSpan Function(BuildContext, TextBlockItem);
/// configuration can be customized to match the visual design of the app. The default
/// configuration is based on the Material Design guidelines.
///
/// Note that the configuration is shared across all instances of the [PortableText] widget.
/// A single global default lives in [shared]; individual subtrees can override the rendering
/// configuration by wrapping them in a [PortableTextTheme] (resolved via [of]).
final class PortableTextConfig {
/// The styles used to render the Portable Text content. The keys are the style names used
/// in the Portable Text content, such as "h1", "h2", "blockquote", etc. The default styles
Expand Down Expand Up @@ -63,15 +64,68 @@ final class PortableTextConfig {
/// The base style used for rendering the Portable Text content. The default value is the bodyMedium style from the theme.
TextStyle? Function(BuildContext) baseStyle = defaultBaseStyle;

/// The shared instance of the PortableTextConfig. This instance is used by all [PortableText] widgets
/// in the application. You can customize the configuration by calling the [apply] method.
static final PortableTextConfig shared = PortableTextConfig._();
/// The default/root configuration, used by [PortableText] widgets when no [PortableTextTheme]
/// ancestor provides one (see [of]). It is also the context-free registry consulted at
/// JSON-parse time for custom mark deserializers (see `_markDefsFromJson`). You can customize
/// it by calling the [apply] method.
static final PortableTextConfig shared = PortableTextConfig();

/// The bullet renderer used to render the bullet for list items. The default value is a simple bullet renderer
/// that handles the default bullet types: number, square, and circle.
BulletRenderer bulletRenderer = defaultBulletRenderer;

PortableTextConfig._();
PortableTextConfig();

/// Returns the nearest [PortableTextConfig] supplied by a [PortableTextTheme] ancestor,
/// falling back to [shared] when none is present. Mirrors `Theme.of`.
static PortableTextConfig of(final BuildContext context) =>
PortableTextTheme.maybeOf(context) ?? shared;

/// Returns a copy of this config with the given fields overridden.
///
/// The map fields ([styles], [blocks], [blockContainers], [markDefs]) are MERGED — provided
/// keys win, the rest are kept — and scalar fields are replaced when provided. This
/// intentionally differs from `ThemeData.copyWith` (which replaces whole fields) because
/// these maps are additive registries, so the common case is overriding a single key while
/// keeping the others.
PortableTextConfig copyWith({
final Map<String, TextStyleBuilder>? styles,
final Map<String, BlockWidgetBuilder>? blocks,
final Map<String, BlockContainerBuilder>? blockContainers,
final Map<String, MarkDefDescriptor>? markDefs,
final double? listIndent,
final EdgeInsets? itemPadding,
final TextStyle? Function(BuildContext)? baseStyle,
final BulletRenderer? bulletRenderer,
}) {
final config = PortableTextConfig();
config.styles
..clear()
..addAll(this.styles);
config.blocks
..clear()
..addAll(this.blocks);
config.blockContainers
..clear()
..addAll(this.blockContainers);
config.markDefs
..clear()
..addAll(this.markDefs);
config.listIndent = this.listIndent;
config.itemPadding = this.itemPadding;
config.baseStyle = this.baseStyle;
config.bulletRenderer = this.bulletRenderer;

if (styles != null) config.styles.addAll(styles);
if (blocks != null) config.blocks.addAll(blocks);
if (blockContainers != null) config.blockContainers.addAll(blockContainers);
if (markDefs != null) config.markDefs.addAll(markDefs);
if (listIndent != null) config.listIndent = listIndent;
if (itemPadding != null) config.itemPadding = itemPadding;
if (baseStyle != null) config.baseStyle = baseStyle;
if (bulletRenderer != null) config.bulletRenderer = bulletRenderer;
return config;
}

/// Applies the custom configuration to the shared instance of the [PortableTextConfig].
void apply({
Expand Down Expand Up @@ -131,7 +185,7 @@ final class PortableTextConfig {
static const defaultItemPadding = EdgeInsets.only(bottom: 8);
static BulletRenderer defaultBulletRenderer =
(final BuildContext context, final TextBlockItem model) {
final textStyle = PortableTextConfig.shared.baseStyle(context);
final textStyle = PortableTextConfig.of(context).baseStyle(context);

switch (model.listItem) {
case ListItemType.number:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';

import '../flutter_sanity_portable_text.dart';

/// An [InheritedWidget] that supplies a [PortableTextConfig] to its subtree.
///
/// Mirrors Flutter's `Theme` / `Theme.of`: descendant Portable Text widgets resolve their
/// config via [PortableTextConfig.of], which returns the nearest [PortableTextTheme]'s
/// [config] or falls back to [PortableTextConfig.shared]. Wrap a subtree to render it with a
/// different configuration without mutating the global [PortableTextConfig.shared].
class PortableTextTheme extends InheritedWidget {
/// The configuration applied to descendant Portable Text widgets.
final PortableTextConfig config;

const PortableTextTheme({
super.key,
required this.config,
required super.child,
});

/// The [PortableTextConfig] of the nearest [PortableTextTheme] ancestor, or `null` if none.
static PortableTextConfig? maybeOf(final BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<PortableTextTheme>()?.config;

@override
bool updateShouldNotify(final PortableTextTheme oldWidget) =>
config != oldWidget.config;
}

/// Restyles descendant Portable Text by transforming the *inherited* text style for each
/// named style (e.g. `'normal'`, `'h2'`).
///
/// Each entry receives the resolved style produced by the ambient config's builder and
/// returns the adjusted style, so callers express only the delta:
///
/// ```dart
/// PortableTextStyleOverride(
/// styles: {'normal': (s) => s.copyWith(fontSize: (s.fontSize ?? 16) - 2)},
/// child: ...,
/// )
/// ```
///
/// The inherited builder is captured internally, so referencing the same style name does
/// not recurse. Everything else (blocks, marks, spacing, parsing) is inherited unchanged.
class PortableTextStyleOverride extends StatelessWidget {
/// Per-style transforms applied on top of the inherited resolved style. Keys are style
/// names (`'normal'`, `'h2'`, …); a key that the ambient config does not define is ignored.
final Map<String, TextStyle Function(TextStyle)> styles;

/// The subtree whose Portable Text should be restyled.
final Widget child;

const PortableTextStyleOverride({
super.key,
required this.styles,
required this.child,
});

@override
Widget build(final BuildContext context) {
final base = PortableTextConfig.of(context);
final wrapped = <String, TextStyleBuilder>{
for (final entry in styles.entries)
if (base.styles[entry.key] case final builder?)
entry.key: (final ctx, final inherited) =>
entry.value(builder(ctx, inherited)),
};
return PortableTextTheme(
config: base.copyWith(styles: wrapped),
child: child,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ Widget defaultListBuilder(
itemCount: blocks.length,
padding: EdgeInsets.zero,
itemBuilder: (final context, final index) =>
PortableTextConfig.shared.buildBlock(context, blocks[index]),
PortableTextConfig.of(context).buildBlock(context, blocks[index]),
);
}
1 change: 0 additions & 1 deletion packages/sanity/flutter_sanity_portable_text/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
name: flutter_sanity_portable_text
resolution: workspace
version: 1.8.1
description: |
Flutter renderer for the Sanity.io Portable Text format with support for
Expand Down