/// The shared vocabulary every internal page is built from. Internal pages are /// ordinary Mews documents, so a page has only headings, lists, paragraphs and /// links to work with; these builders keep the result consistent. library; import '../../l10n/strings.dart'; import '../../mews/document.dart'; import 'mews_urls.dart'; /// Separates independent labels on one line. A separator is layout, so it is a /// constant here rather than a message. const actionSeparator = ' '; /// A link with its label: a choice in a group, or an action on a row. For an /// action [current] is always false. typedef Choice = ({String href, String label, bool current}); /// An action link on a row. Choice action(String href, String label) => (href: href, label: label, current: false); /// One entry in the strip of internal pages. typedef NavItem = ({String url, String labelId}); /// The strip carried by every internal page, in a fixed order so its position /// never shifts under the reader. const navItems = [ (url: MewsUrls.hub, labelId: 'nav-hub'), (url: MewsUrls.posts, labelId: 'nav-posts'), (url: MewsUrls.bookmarks, labelId: 'nav-bookmarks'), (url: MewsUrls.subscriptions, labelId: 'nav-subscriptions'), (url: MewsUrls.history, labelId: 'nav-history'), (url: MewsUrls.settings, labelId: 'nav-settings'), ]; /// The title and the navigation strip that open every internal page. The page /// the reader is on is plain text, matching how a current value is marked. List chrome({ required String title, required Uri current, required Strings strings, }) { final inline = []; for (final item in navItems) { if (inline.isNotEmpty) inline.add(const MewsText(actionSeparator)); final label = strings.t(item.labelId); // Compare paths so a filtered or sorted page still marks its own entry. if (Uri.parse(item.url).path == current.path) { inline.add(MewsEmphasis(EmphasisStyle.strong, [MewsText(label)])); } else { inline.add(MewsLink(item.url, [MewsText(label)])); } } return [ HeadingBlock(1, [MewsText(title)]), ParagraphBlock(inline), const RuleBlock(), ]; } /// One row in a list: the item, then an optional note on its own line. A date /// goes inside the link, the way a Mews page writes its own dated links (spec /// 6.1), so the whole row is one target. One paragraph per row keeps rows /// close together, since each paragraph carries its own bottom padding. MewsListItem itemRow({ required String href, required String label, String? date, String? note, bool strong = false, List actions = const [], }) { final text = date == null ? label : '$date $label'; return MewsListItem([ ParagraphBlock([ MewsLink(href, [ if (strong) MewsEmphasis(EmphasisStyle.strong, [MewsText(text)]) else MewsText(text), ]), if (note != null) ...[ const MewsLineBreak(), MewsEmphasis(EmphasisStyle.emphasis, [MewsText(note)]), ], if (actions.isNotEmpty) ...[ const MewsLineBreak(), ..._separated( [for (final action in actions) MewsLink(action.href, [MewsText(action.label)])]), ], ]), ]); } /// Joins runs with the separator between them, never around them. List _separated(List runs) { final out = []; for (final run in runs) { if (out.isNotEmpty) out.add(const MewsText(actionSeparator)); out.add(run); } return out; } /// A group of mutually exclusive values on one line, for a short group that /// would waste a list. The chosen value is plain text, the rest are links. List choiceLine({required String label, required List options}) { return [ ParagraphBlock([ MewsEmphasis(EmphasisStyle.emphasis, [MewsText(label)]), const MewsText(actionSeparator), ..._separated([ for (final option in options) if (option.current) MewsEmphasis(EmphasisStyle.strong, [MewsText(option.label)]) else MewsLink(option.href, [MewsText(option.label)]), ]), ]), ]; } /// A group of mutually exclusive values: the current one plain, the rest links. ListBlock choiceRow(List options) { return ListBlock( ordered: false, start: 1, items: [ for (final option in options) MewsListItem([ ParagraphBlock([ if (option.current) MewsEmphasis(EmphasisStyle.strong, [MewsText(option.label)]) else MewsLink(option.href, [MewsText(option.label)]), ]), ]), ], ); } /// A plain list of links, used for menus and library lists alike. ListBlock itemList(List items) => ListBlock(ordered: false, start: 1, items: items); /// Wraps blocks as an internal document. The marker makes the renderer treat /// it as a Mews page rather than prepending the "not a Mews page" notice, and /// no entries are offered so an internal page is never subscribable. MewsPage internalPage({ required Uri url, required String title, required List body, }) { return MewsPage( url: url.toString(), specVersion: 'internal', title: title, body: body, lang: 'en', direction: TextDirection.ltr, variant: MewsVariant.plain, entries: const [], ); } /// Picks the message for [count]. Plurals are separate messages, so the /// choice happens here and Fluent formats the number. String countText( Strings strings, int count, { required String none, required String one, required String many, }) { return switch (count) { 0 => strings.t(none), 1 => strings.t(one), _ => strings.t(many, {'count': count}), }; } /// YYYY-MM-DD, the shape a Mews entry link uses (spec 6.1). String dayText(DateTime date) => '${date.year.toString().padLeft(4, '0')}-${_two(date.month)}-${_two(date.day)}'; String clockText(DateTime date) => '${_two(date.hour)}:${_two(date.minute)}'; /// The day and the time of day, for history rows. String dayTimeText(DateTime date) => '${dayText(date)} ${clockText(date)}'; String _two(int value) => value.toString().padLeft(2, '0');