diff --git a/site/lib/_sass/components/_button.scss b/site/lib/_sass/components/_button.scss index 530dca309a1..aa37b998529 100644 --- a/site/lib/_sass/components/_button.scss +++ b/site/lib/_sass/components/_button.scss @@ -94,3 +94,22 @@ button { } } } + +.segmented-button { + display: inline-flex; + + a, + button { + &:not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + + border-right: 1px solid var(--site-outline-variant); + } + + &:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } +} diff --git a/site/lib/_sass/components/_code.scss b/site/lib/_sass/components/_code.scss index 1d4a1ee83f2..b77aa5e00ce 100644 --- a/site/lib/_sass/components/_code.scss +++ b/site/lib/_sass/components/_code.scss @@ -276,22 +276,50 @@ pre { } .code-block-wrapper { - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: min-content 1fr; + grid-template-columns: 100%; margin-block-start: 1rem; margin-block-end: 1rem; border: 1px solid var(--site-inset-borderColor); background-color: var(--site-inset-bgColor); + transition: grid-template-rows 0.3s ease, border-bottom-width 0.3s ease; + + &.collapsed { + grid-template-rows: min-content 0fr; + border-bottom-width: 0px; + + .collapse-button > .material-symbols { + transform: rotate(180deg); + } + } + + .collapse-button > .material-symbols { + transform: rotate(0deg); + transform-origin: center; + transition: transform 0.3s ease; + } + .code-block-header { + display: flex; + align-items: center; + background-color: var(--site-raised-bgColor); border-bottom: 1px solid var(--site-inset-borderColor); - font-size: 0.9375rem; - font-weight: 500; - overflow-x: hidden; - text-overflow: ellipsis; + padding: 0.75rem 0.5rem 0.67rem 1rem; + gap: 0.5rem; + + > span:first-child { + flex-grow: 1; + overflow-x: hidden; + text-overflow: ellipsis; + + font-size: 0.9375rem; + font-weight: 500; + } } .code-block-body { @@ -299,6 +327,8 @@ pre { position: relative; background: none; + min-height: 0; + overflow: hidden; .copy-button { position: absolute; diff --git a/site/lib/jaspr_options.dart b/site/lib/jaspr_options.dart index 5ef15399edc..3ebd94e678f 100644 --- a/site/lib/jaspr_options.dart +++ b/site/lib/jaspr_options.dart @@ -7,41 +7,45 @@ import 'package:jaspr/jaspr.dart'; import 'package:docs_flutter_dev_site/src/client/global_scripts.dart' as prefix0; -import 'package:docs_flutter_dev_site/src/components/common/client/cookie_notice.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/collapse_button.dart' as prefix1; -import 'package:docs_flutter_dev_site/src/components/common/client/copy_button.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/cookie_notice.dart' as prefix2; -import 'package:docs_flutter_dev_site/src/components/common/client/download_latest_button.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/copy_button.dart' as prefix3; -import 'package:docs_flutter_dev_site/src/components/common/client/feedback.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/download_button.dart' as prefix4; -import 'package:docs_flutter_dev_site/src/components/common/client/on_this_page_button.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/download_latest_button.dart' as prefix5; -import 'package:docs_flutter_dev_site/src/components/common/client/os_selector.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/feedback.dart' as prefix6; -import 'package:docs_flutter_dev_site/src/components/common/client/simple_tooltip.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/on_this_page_button.dart' as prefix7; -import 'package:docs_flutter_dev_site/src/components/dartpad/dartpad_injector.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/os_selector.dart' as prefix8; -import 'package:docs_flutter_dev_site/src/components/layout/client/pagenav.dart' +import 'package:docs_flutter_dev_site/src/components/common/client/simple_tooltip.dart' as prefix9; -import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart' +import 'package:docs_flutter_dev_site/src/components/dartpad/dartpad_injector.dart' as prefix10; -import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart' +import 'package:docs_flutter_dev_site/src/components/layout/client/pagenav.dart' as prefix11; -import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart' +import 'package:docs_flutter_dev_site/src/components/layout/menu_toggle.dart' as prefix12; -import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart' +import 'package:docs_flutter_dev_site/src/components/layout/site_switcher.dart' as prefix13; -import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart' +import 'package:docs_flutter_dev_site/src/components/layout/theme_switcher.dart' as prefix14; -import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters.dart' +import 'package:docs_flutter_dev_site/src/components/pages/archive_table.dart' as prefix15; -import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart' +import 'package:docs_flutter_dev_site/src/components/pages/glossary_search_section.dart' as prefix16; -import 'package:docs_flutter_dev_site/src/components/tutorial/client/quiz.dart' +import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters.dart' as prefix17; -import 'package:jaspr_content/components/file_tree.dart' as prefix18; +import 'package:docs_flutter_dev_site/src/components/pages/learning_resource_filters_sidebar.dart' + as prefix18; +import 'package:docs_flutter_dev_site/src/components/tutorial/client/quiz.dart' + as prefix19; +import 'package:jaspr_content/components/file_tree.dart' as prefix20; /// Default [JasprOptions] for use with your jaspr project. /// @@ -65,119 +69,135 @@ JasprOptions get defaultJasprOptions => JasprOptions( 'src/client/global_scripts', ), - prefix1.CookieNotice: ClientTarget( + prefix1.CollapseButton: ClientTarget( + 'src/components/common/client/collapse_button', + params: _prefix1CollapseButton, + ), + + prefix2.CookieNotice: ClientTarget( 'src/components/common/client/cookie_notice', ), - prefix2.CopyButton: ClientTarget( + prefix3.CopyButton: ClientTarget( 'src/components/common/client/copy_button', - params: _prefix2CopyButton, + params: _prefix3CopyButton, + ), + + prefix4.DownloadButton: ClientTarget( + 'src/components/common/client/download_button', + params: _prefix4DownloadButton, ), - prefix3.DownloadLatestButton: ClientTarget( + prefix5.DownloadLatestButton: ClientTarget( 'src/components/common/client/download_latest_button', - params: _prefix3DownloadLatestButton, + params: _prefix5DownloadLatestButton, ), - prefix4.FeedbackComponent: ClientTarget( + prefix6.FeedbackComponent: ClientTarget( 'src/components/common/client/feedback', - params: _prefix4FeedbackComponent, + params: _prefix6FeedbackComponent, ), - prefix5.OnThisPageButton: ClientTarget( + prefix7.OnThisPageButton: ClientTarget( 'src/components/common/client/on_this_page_button', ), - prefix6.OsSelector: ClientTarget( + prefix8.OsSelector: ClientTarget( 'src/components/common/client/os_selector', ), - prefix7.SimpleTooltip: ClientTarget( + prefix9.SimpleTooltip: ClientTarget( 'src/components/common/client/simple_tooltip', - params: _prefix7SimpleTooltip, + params: _prefix9SimpleTooltip, ), - prefix8.DartPadInjector: ClientTarget( + prefix10.DartPadInjector: ClientTarget( 'src/components/dartpad/dartpad_injector', - params: _prefix8DartPadInjector, + params: _prefix10DartPadInjector, ), - prefix9.PageNav: ClientTarget( + prefix11.PageNav: ClientTarget( 'src/components/layout/client/pagenav', - params: _prefix9PageNav, + params: _prefix11PageNav, ), - prefix10.MenuToggle: ClientTarget( + prefix12.MenuToggle: ClientTarget( 'src/components/layout/menu_toggle', ), - prefix11.SiteSwitcher: ClientTarget( + prefix13.SiteSwitcher: ClientTarget( 'src/components/layout/site_switcher', ), - prefix12.ThemeSwitcher: ClientTarget( + prefix14.ThemeSwitcher: ClientTarget( 'src/components/layout/theme_switcher', ), - prefix13.ArchiveTable: ClientTarget( + prefix15.ArchiveTable: ClientTarget( 'src/components/pages/archive_table', - params: _prefix13ArchiveTable, + params: _prefix15ArchiveTable, ), - prefix14.GlossarySearchSection: - ClientTarget( + prefix16.GlossarySearchSection: + ClientTarget( 'src/components/pages/glossary_search_section', ), - prefix15.LearningResourceFilters: - ClientTarget( + prefix17.LearningResourceFilters: + ClientTarget( 'src/components/pages/learning_resource_filters', ), - prefix16.LearningResourceFiltersSidebar: - ClientTarget( + prefix18.LearningResourceFiltersSidebar: + ClientTarget( 'src/components/pages/learning_resource_filters_sidebar', ), - prefix17.InteractiveQuiz: ClientTarget( + prefix19.InteractiveQuiz: ClientTarget( 'src/components/tutorial/client/quiz', - params: _prefix17InteractiveQuiz, + params: _prefix19InteractiveQuiz, ), }, - styles: () => [...prefix18.FileTree.styles], + styles: () => [...prefix20.FileTree.styles], ); -Map _prefix2CopyButton(prefix2.CopyButton c) => { - 'toCopy': c.toCopy, +Map _prefix1CollapseButton(prefix1.CollapseButton c) => { + 'classes': c.classes, + 'title': c.title, +}; +Map _prefix3CopyButton(prefix3.CopyButton c) => { 'buttonText': c.buttonText, 'classes': c.classes, 'title': c.title, }; -Map _prefix3DownloadLatestButton( - prefix3.DownloadLatestButton c, +Map _prefix4DownloadButton(prefix4.DownloadButton c) => { + 'name': c.name, +}; +Map _prefix5DownloadLatestButton( + prefix5.DownloadLatestButton c, ) => {'os': c.os, 'arch': c.arch}; -Map _prefix4FeedbackComponent(prefix4.FeedbackComponent c) => { +Map _prefix6FeedbackComponent(prefix6.FeedbackComponent c) => { 'issueUrl': c.issueUrl, }; -Map _prefix7SimpleTooltip(prefix7.SimpleTooltip c) => { +Map _prefix9SimpleTooltip(prefix9.SimpleTooltip c) => { 'target': c.target.toId(), 'content': c.content.toId(), }; -Map _prefix8DartPadInjector(prefix8.DartPadInjector c) => { +Map _prefix10DartPadInjector(prefix10.DartPadInjector c) => { 'title': c.title, 'theme': c.theme, 'height': c.height, 'runAutomatically': c.runAutomatically, }; -Map _prefix9PageNav(prefix9.PageNav c) => { +Map _prefix11PageNav(prefix11.PageNav c) => { 'title': c.title, 'content': c.content.toId(), }; -Map _prefix13ArchiveTable(prefix13.ArchiveTable c) => { +Map _prefix15ArchiveTable(prefix15.ArchiveTable c) => { 'os': c.os, 'channel': c.channel, }; -Map _prefix17InteractiveQuiz(prefix17.InteractiveQuiz c) => { +Map _prefix19InteractiveQuiz(prefix19.InteractiveQuiz c) => { 'title': c.title, 'questions': c.questions.map((i) => i.toJson()).toList(), }; diff --git a/site/lib/main.dart b/site/lib/main.dart index 217c6341a09..32bda5b1a21 100644 --- a/site/lib/main.dart +++ b/site/lib/main.dart @@ -20,6 +20,7 @@ import 'src/components/pages/archive_table.dart'; import 'src/components/pages/devtools_release_notes_index.dart'; import 'src/components/pages/expansion_list.dart'; import 'src/components/pages/learning_resource_index.dart'; +import 'src/components/tutorial/downloadable_snippet.dart'; import 'src/components/tutorial/progress_ring.dart'; import 'src/components/tutorial/quiz.dart'; import 'src/components/tutorial/summary_card.dart'; @@ -105,6 +106,7 @@ List get _embeddableComponents => [ const Quiz(), const ProgressRing(), const SummaryCard(), + const DownloadableSnippet(), CustomComponent( pattern: RegExp('OSSelector', caseSensitive: false), builder: (_, _, _) => const OsSelector(), diff --git a/site/lib/src/components/common/button.dart b/site/lib/src/components/common/button.dart index f6320f0730b..1420f5bae84 100644 --- a/site/lib/src/components/common/button.dart +++ b/site/lib/src/components/common/button.dart @@ -94,3 +94,17 @@ enum ButtonStyle { ButtonStyle.text => 'text-button', }; } + +class SegmentedButton extends StatelessComponent { + const SegmentedButton({ + super.key, + required this.children, + }); + + final List children; + + @override + Component build(BuildContext context) { + return span(classes: ['segmented-button'].toClasses, children); + } +} diff --git a/site/lib/src/components/common/client/collapse_button.dart b/site/lib/src/components/common/client/collapse_button.dart new file mode 100644 index 00000000000..e87e03d861b --- /dev/null +++ b/site/lib/src/components/common/client/collapse_button.dart @@ -0,0 +1,48 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/jaspr.dart'; + +import 'package:universal_web/web.dart' as web; + +import '../button.dart'; + +@client +class CollapseButton extends StatefulComponent { + const CollapseButton({ + this.classes = const [], + this.title, + }); + + final String? title; + final List classes; + + @override + State createState() => _CollapseButtonState(); +} + +class _CollapseButtonState extends State { + final buttonKey = GlobalNodeKey(); + bool _collapsed = true; + + void toggleCollapse() { + setState(() => _collapsed = !_collapsed); + + buttonKey.currentNode + ?.closest('.code-block-wrapper') + ?.classList + .toggle('collapsed'); + } + + @override + Component build(BuildContext _) { + return Button( + ref: buttonKey, + classes: ['collapse-button', ...component.classes], + title: component.title, + icon: 'keyboard_arrow_up', + onClick: toggleCollapse, + ); + } +} diff --git a/site/lib/src/components/common/client/copy_button.dart b/site/lib/src/components/common/client/copy_button.dart index 8fdaf2d306f..e8b6416a46d 100644 --- a/site/lib/src/components/common/client/copy_button.dart +++ b/site/lib/src/components/common/client/copy_button.dart @@ -11,13 +11,11 @@ import '../button.dart'; @client class CopyButton extends StatefulComponent { const CopyButton({ - required this.toCopy, this.buttonText, this.classes = const [], this.title, }); - final String toCopy; final String? title; final String? buttonText; final List classes; @@ -27,21 +25,47 @@ class CopyButton extends StatefulComponent { } class _CopyButtonState extends State { - bool _hidden = true; + final buttonKey = GlobalNodeKey(); + + String? content; bool _copied = false; + static final RegExp _terminalReplacementPattern = RegExp( + r'^(\s*\$\s*)|(PS\s+)?(C:\\(.*)>\s*)', + multiLine: true, + ); + @override void initState() { - // Unhide the copy button if successfully initialized on the client. - if (kIsWeb && component.toCopy.isNotEmpty) { - _hidden = false; + if (kIsWeb) { + // Extract the code content and unhide the copy button on the client. + context.binding.addPostFrameCallback(() { + setState(() { + content = buttonKey.currentNode + ?.closest('.code-block-wrapper') + ?.querySelector('pre code') + ?.textContent + ?.replaceAll(_terminalReplacementPattern, '') + .replaceAll('\u200B', ''); // Remove zero-width spaces + }); + + assert( + content != null, + 'CopyButton: Unable to find code content to copy. ' + 'Is the CopyButton inside a code block?', + ); + }); } super.initState(); } void _copy() { - web.window.navigator.clipboard.writeText(component.toCopy); + if (content == null) { + return; + } + + web.window.navigator.clipboard.writeText(content!); setState(() => _copied = true); @@ -57,13 +81,14 @@ class _CopyButtonState extends State { final iconButton = component.buttonText == null; return Button( + ref: buttonKey, style: iconButton ? ButtonStyle.text : ButtonStyle.filled, classes: [ 'copy-button', - if (_hidden) 'hidden', + if (content == null) 'hidden', ...component.classes, ], - title: component.title ?? 'Copy ${component.toCopy} to your clipboard.', + title: component.title ?? 'Copy $content to your clipboard.', content: _copied ? 'Copied!' : component.buttonText, icon: iconButton ? 'content_copy' : null, onClick: _copy, diff --git a/site/lib/src/components/common/client/download_button.dart b/site/lib/src/components/common/client/download_button.dart new file mode 100644 index 00000000000..300bc4ec44e --- /dev/null +++ b/site/lib/src/components/common/client/download_button.dart @@ -0,0 +1,119 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/jaspr.dart'; +import 'package:universal_web/js_interop.dart'; +import 'package:universal_web/web.dart' as web; + +import '../button.dart'; + +@client +class DownloadButton extends StatefulComponent { + const DownloadButton({ + required this.name, + super.key, + }); + + final String name; + + @override + State createState() => _DownloadButtonState(); +} + +class _DownloadButtonState extends State { + final buttonKey = GlobalNodeKey(); + + Future saveAsFile() async { + final content = buttonKey.currentNode + ?.closest('.code-block-wrapper') + ?.querySelector('pre code') + ?.textContent + ?.replaceAll('\u200B', ''); // Remove zero-width spaces + + if (content == null) { + return; + } + + if (rawShowSaveFilePicker != null) { + try { + final file = await showSaveFilePicker( + SaveFilePickerOptions( + id: 'download-project-file', + startIn: 'documents', + suggestedName: component.name, + types: [ + FilePickerAcceptType( + description: 'Dart', + accept: { + 'text/plain': ['.dart'], + }.jsify(), + ), + ].toJS, + ), + ).toDart; + + final writable = await file.createWritable().toDart; + await writable.write(content.toJS).toDart; + await writable.close().toDart; + } catch (_) { + // User cancelled the picker + } + } else { + // Fallback for browsers that do not support the File System API + + final blob = web.Blob( + [content.toJS].toJS, + web.BlobPropertyBag(type: 'text/plain'), + ); + final objectUrl = web.URL.createObjectURL(blob); + + final anchor = web.document.createElement('a') as web.HTMLAnchorElement; + anchor.href = objectUrl; + anchor.download = component.name; + anchor.style.display = 'none'; + + web.document.body?.append(anchor); + anchor.click(); + + anchor.remove(); + web.URL.revokeObjectURL(objectUrl); + } + } + + @override + Component build(BuildContext context) { + return Button( + ref: buttonKey, + onClick: saveAsFile, + style: ButtonStyle.filled, + icon: 'download', + title: 'Download file', + content: 'Download file', + ); + } +} + +@JS('showSaveFilePicker') +external JSFunction? rawShowSaveFilePicker; + +@JS() +external JSPromise showSaveFilePicker( + SaveFilePickerOptions options, +); + +extension type SaveFilePickerOptions._(JSObject _) implements JSObject { + external factory SaveFilePickerOptions({ + String? id, + String? startIn, + String? suggestedName, + JSArray? types, + }); +} + +extension type FilePickerAcceptType._(JSObject _) implements JSObject { + external factory FilePickerAcceptType({ + required String description, + required JSAny? accept, + }); +} diff --git a/site/lib/src/components/common/wrapped_code_block.dart b/site/lib/src/components/common/wrapped_code_block.dart index 63787c12752..b1f95b91373 100644 --- a/site/lib/src/components/common/wrapped_code_block.dart +++ b/site/lib/src/components/common/wrapped_code_block.dart @@ -5,6 +5,7 @@ import 'package:jaspr/jaspr.dart'; import '../../util.dart'; +import 'client/collapse_button.dart'; import 'client/copy_button.dart'; import 'material_icon.dart'; @@ -25,11 +26,12 @@ final class WrappedCodeBlock extends StatelessComponent { this.tag, this.initialLineNumber = 1, this.showLineNumbers = false, - this.textToCopy, + this.showCopyButton = true, + this.collapsed = false, + this.actions = const [], }); final List> content; - final String? textToCopy; final String language; final String? title; @@ -43,6 +45,9 @@ final class WrappedCodeBlock extends StatelessComponent { final int initialLineNumber; final bool showLineNumbers; + final bool showCopyButton; + final bool collapsed; + final List actions; @override Component build(BuildContext context) { @@ -101,11 +106,18 @@ final class WrappedCodeBlock extends StatelessComponent { } return div( - classes: 'code-block-wrapper language-$language', + classes: [ + 'code-block-wrapper language-$language', + if (collapsed) 'collapsed', + ].toClasses, [ if (title case final title?) div(classes: 'code-block-header', [ - text(title), + span([ + text(title), + ]), + if (actions.isNotEmpty) ...actions, + if (collapsed) const CollapseButton(), ]), div( classes: [ @@ -137,9 +149,8 @@ final class WrappedCodeBlock extends StatelessComponent { ]), ], ), - if (textToCopy case final textToCopy?) - CopyButton( - toCopy: textToCopy, + if (showCopyButton) + const CopyButton( title: 'Copy code to clipboard', ), ], diff --git a/site/lib/src/components/tutorial/downloadable_snippet.dart b/site/lib/src/components/tutorial/downloadable_snippet.dart new file mode 100644 index 00000000000..b11391a25b3 --- /dev/null +++ b/site/lib/src/components/tutorial/downloadable_snippet.dart @@ -0,0 +1,77 @@ +// Copyright 2025 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:jaspr/jaspr.dart'; +import 'package:jaspr_content/jaspr_content.dart'; +import 'package:path/path.dart' as path; + +import '../../extensions/code_block_processor.dart'; +import '../../util.dart'; +import '../common/client/copy_button.dart'; +import '../common/client/download_button.dart'; +import '../common/wrapped_code_block.dart'; + +class DownloadableSnippet extends CustomComponentBase { + const DownloadableSnippet(); + + @override + Pattern get pattern => 'DownloadableSnippet'; + + @override + Component apply(_, Map attributes, _) { + final src = attributes['src']; + final name = attributes['name']; + + if (src == null || name == null) { + throw ArgumentError( + 'SnippetDownloadButton requires "src" and "name" attributes.', + ); + } + + return Builder( + builder: (context) { + final page = context.page; + final snippet = page.loader.readPartialSync( + path.join(siteSrcDirectoryPath, '_snippets', src), + page, + ); + final language = src.split('.').last; + + final processedContent = CodeBlockProcessor.highlightCode( + snippet + .split('\n') + .map((l) => CodeLine(content: l, highlights: const [])) + .toList(), + language: language, + ); + + final codeBlock = WrappedCodeBlock( + content: processedContent, + language: language, + languagesToHide: const { + 'plaintext', + 'text', + 'console', + 'ps', + 'diff', + }, + title: name, + showLineNumbers: true, + showCopyButton: false, + collapsed: true, + actions: [ + DownloadButton( + name: name, + ), + const CopyButton( + title: 'Copy to clipboard', + ), + ], + ); + + return codeBlock; + }, + ); + } +} diff --git a/site/lib/src/extensions/code_block_processor.dart b/site/lib/src/extensions/code_block_processor.dart index 023aa1e1392..aa1668e5740 100644 --- a/site/lib/src/extensions/code_block_processor.dart +++ b/site/lib/src/extensions/code_block_processor.dart @@ -101,7 +101,7 @@ final class CodeBlockProcessor implements PageExtension { foldingResult.lines, skipHighlighting, ); - final processedContent = _highlightCode( + final processedContent = highlightCode( codeLines, language: language, skipSyntaxHighlighting: skipHighlighting, @@ -110,7 +110,6 @@ final class CodeBlockProcessor implements PageExtension { return ComponentNode( WrappedCodeBlock( content: processedContent, - textToCopy: codeLines.copyContent, language: language, languagesToHide: const { 'plaintext', @@ -141,8 +140,8 @@ final class CodeBlockProcessor implements PageExtension { ); } - List> _highlightCode( - List<_CodeLine> codeLines, { + static List> highlightCode( + List codeLines, { required String language, bool skipSyntaxHighlighting = false, }) { @@ -165,7 +164,7 @@ final class CodeBlockProcessor implements PageExtension { ]; } - List _processLine( + static List _processLine( List spans, List<({int startColumn, int length})> highlights, ) { @@ -197,7 +196,7 @@ final class CodeBlockProcessor implements PageExtension { return processedSpans; } - List<({int startColumn, int length})> _findIntersectingHighlights( + static List<({int startColumn, int length})> _findIntersectingHighlights( List<({int startColumn, int length})> highlights, int spanStart, int spanEnd, @@ -208,7 +207,7 @@ final class CodeBlockProcessor implements PageExtension { }) .sorted((a, b) => a.startColumn.compareTo(b.startColumn)); - List _splitSpanByHighlights( + static List _splitSpanByHighlights( highlighter.ThemedSpan span, List<({int startColumn, int length})> highlights, int spanStart, @@ -266,7 +265,7 @@ final class CodeBlockProcessor implements PageExtension { return result; } - jaspr.Component _createSpan( + static jaspr.Component _createSpan( highlighter.ThemedSpan span, { String? content, }) { @@ -293,13 +292,13 @@ final class CodeBlockProcessor implements PageExtension { .toList(growable: false); } - List<_CodeLine> _removeHighlights( + List _removeHighlights( List lines, [ bool skipHighlighting = false, ]) { if (skipHighlighting) { return lines - .map((line) => _CodeLine(content: line, highlights: const [])) + .map((line) => CodeLine(content: line, highlights: const [])) .toList(growable: false); } @@ -404,7 +403,7 @@ final class CodeBlockProcessor implements PageExtension { return [ for (var i = 0; i < processedLines.length; i++) - _CodeLine( + CodeLine( content: processedLines[i], highlights: lineHighlights[i] ?? [], ), @@ -508,24 +507,11 @@ final class CodeBlockProcessor implements PageExtension { } @immutable -final class _CodeLine { +final class CodeLine { final String content; final List<({int startColumn, int length})> highlights; - const _CodeLine({required this.content, required this.highlights}); -} - -extension on List<_CodeLine> { - static final RegExp _terminalReplacementPattern = RegExp( - r'^(\s*\$\s*)|(PS\s+)?(C:\\(.*)>\s*)', - multiLine: true, - ); - static final RegExp _zeroWidthSpaceReplacementPattern = RegExp(r'\u200B'); - - String get copyContent => map((line) => line.content) - .join('\n') - .replaceAll(_terminalReplacementPattern, '') - .replaceAll(_zeroWidthSpaceReplacementPattern, ''); + const CodeLine({required this.content, required this.highlights}); } /// Parses a comma-separated list of numbers and ranges into a set of numbers. diff --git a/site/lib/src/style_hash.dart b/site/lib/src/style_hash.dart index d1f6c46bdd6..7d5ca9d899d 100644 --- a/site/lib/src/style_hash.dart +++ b/site/lib/src/style_hash.dart @@ -2,4 +2,4 @@ // dart format off /// The generated hash of the `main.css` file. -const generatedStylesHash = 'xNb0lgKhtevj'; +const generatedStylesHash = 'FE5OYMbmdUYo'; diff --git a/src/_includes/docs/tutorial/game-code.md b/src/_snippets/tutorial/game-code.dart similarity index 93% rename from src/_includes/docs/tutorial/game-code.md rename to src/_snippets/tutorial/game-code.dart index 91803eaa9a0..c10b97022b2 100644 --- a/src/_includes/docs/tutorial/game-code.md +++ b/src/_snippets/tutorial/game-code.dart @@ -1,17 +1,3 @@ -
- - -
- -```dart import 'dart:collection'; import 'dart:math'; @@ -169,6 +155,7 @@ class Word with IterableMixin { String toStringVerbose() { return _letters.map((l) => '${l.char} - ${l.type.name}').join('\n'); } +} // Domain specific methods that contain word related logic. extension WordUtils on Word { @@ -217,6 +204,7 @@ extension WordUtils on Word { break; } } + } // Mark remaining letters in guessed word as misses for (var i = 0; i < length; i++) { @@ -227,9 +215,4 @@ extension WordUtils on Word { return this; } -} -``` - -
- -
+} \ No newline at end of file diff --git a/src/content/tutorial/ui/2-widget-fundamentals.md b/src/content/tutorial/ui/2-widget-fundamentals.md index 105dc52b97d..1209e60ba00 100644 --- a/src/content/tutorial/ui/2-widget-fundamentals.md +++ b/src/content/tutorial/ui/2-widget-fundamentals.md @@ -22,10 +22,10 @@ widget. This app relies on a bit of game logic that isn't UI-related, and thus is outside the scope of this tutorial. Before you move on, you need to add this logic to your app. -1. Create a new file in the `lib` directory called `game.dart`. -2. Copy the following code into it and import that code into your `main.dart` file. +1. Download the file below and save it as `lib/game.dart` in your project directory. +2. Import the file in your `lib/main.dart` file. -{% render docs/tutorial/game-code.md %} + :::note Game logic note You may notice the lists called `legalGuesses` and `legalWords` only contain a few words. The full lists combined have over 10,000 words, and were omitted for brevity. You don't need the full lists to continue the tutorial. When you're testing your app, make sure to use the few words from those lists.