├── .github └── workflows │ └── main.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h ├── lib │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── test │ └── widget_test.dart └── web │ ├── favicon.png │ ├── icons │ ├── Icon-192.png │ └── Icon-512.png │ ├── index.html │ └── manifest.json ├── lib ├── org_flutter.dart └── src │ ├── controller.dart │ ├── entity.dart │ ├── error.dart │ ├── events.dart │ ├── flash.dart │ ├── folding.dart │ ├── highlight.dart │ ├── indent.dart │ ├── locatable.dart │ ├── locator.dart │ ├── search.dart │ ├── settings.dart │ ├── span.dart │ ├── theme.dart │ ├── util │ ├── alignment.dart │ ├── bidi.dart │ ├── collection.dart │ ├── elisp.dart │ ├── keywords.dart │ ├── local_variables.dart │ ├── locale.dart │ ├── text.dart │ ├── util.dart │ ├── value_notifier.dart │ └── widget.dart │ ├── widget │ ├── org_block.dart │ ├── org_comment.dart │ ├── org_content.dart │ ├── org_decrypted_content.dart │ ├── org_document.dart │ ├── org_drawer.dart │ ├── org_dynamic_block.dart │ ├── org_fixed_width_area.dart │ ├── org_footnote_reference.dart │ ├── org_headline.dart │ ├── org_horizontal_rule.dart │ ├── org_inline_src_block.dart │ ├── org_latex_block.dart │ ├── org_latex_inline.dart │ ├── org_link.dart │ ├── org_link_target.dart │ ├── org_list.dart │ ├── org_local_variables.dart │ ├── org_meta.dart │ ├── org_paragraph.dart │ ├── org_pgp_block.dart │ ├── org_property.dart │ ├── org_radio_target.dart │ ├── org_root.dart │ ├── org_section.dart │ ├── org_sub_superscript.dart │ ├── org_table.dart │ └── org_theme.dart │ └── widgets.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── alignment_test.dart ├── bidi_test.dart ├── elisp_test.dart ├── local_variables_test.dart ├── locale_test.dart ├── text_test.dart └── widget ├── basic_test.dart ├── entities_test.dart ├── events_test.dart ├── footnotes_test.dart ├── headline_test.dart ├── images_test.dart ├── keyword_settings_test.dart ├── link_target_test.dart ├── local_variables_test.dart ├── meta_test.dart ├── named_element_test.dart ├── org-manual.org ├── org_attach_id_dir_test.dart ├── prettification_test.dart ├── radio_link_test.dart ├── search_test.dart ├── section_matcher_test.dart ├── state_restoration_test.dart ├── sub_superscript_test.dart ├── text_changes_test.dart ├── util.dart └── visibility_cycling_test.dart /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: subosito/flutter-action@v2 17 | with: 18 | channel: stable 19 | - name: Install dependencies 20 | run: flutter pub get 21 | - name: Analyze 22 | run: flutter analyze 23 | - name: Run tests 24 | run: make test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | 39 | *~ 40 | /tmp 41 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 659dc8129d4edb9166e9a0d600439d135740933f 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2020 Aaron Madlon-Kay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the “Software”), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | git_hash = $(shell git rev-parse --short HEAD) 2 | gold_dir = $(PWD)/tmp/$(git_hash) 3 | 4 | .PHONY: test 5 | test: ## Run all tests 6 | test: test-unit test-example 7 | 8 | .PHONY: test-unit 9 | test-unit: ## Run unit tests 10 | flutter test 11 | 12 | .PHONY: test-example 13 | test-example: ## Run example tests 14 | cd example && flutter test 15 | 16 | .PHONY: screenshot 17 | screenshot: ## Manually dump screenshots for layout testing purposes 18 | rm -rf $(gold_dir) 19 | flutter test --dart-define=GOLD_DIR=$(gold_dir) --plain-name 'Screenshot' --update-goldens --run-skipped 20 | # Screenshot of entire document is so big that most viewers can't handle it, so 21 | # we split it into 20 parts 22 | cd $(gold_dir) && for f in *.png; do convert -crop 1x20@ +repage $$f $${f%%.png}-%d.png; done 23 | 24 | .PHONY: help 25 | help: ## Show this help text 26 | $(info usage: make [target]) 27 | $(info ) 28 | $(info Available targets:) 29 | @awk -F ':.*?## *' '/^[^\t].+?:.*?##/ \ 30 | {printf " %-24s %s\n", $$1, $$2}' $(MAKEFILE_LIST) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # org_flutter 2 | 3 | [Org Mode](https://orgmode.org/) widgets for Flutter. 4 | 5 | # Usage 6 | 7 | For parsing Org Mode documents, see 8 | [org_parser](https://github.com/amake/org_parser). For an example application 9 | that displays Org Mode documents with org_parser and org_flutter, see 10 | [Orgro](https://orgro.org). 11 | 12 | The simplest way to display an Org Mode document in your Flutter application is 13 | to use the `Org` widget: 14 | 15 | ```dart 16 | import 'package:org_flutter/org_flutter.dart'; 17 | 18 | class MyOrgViewWidget extends StatelessWidget { 19 | Widget build(BuildContext context) { 20 | return Org('''* TODO [#A] foo bar 21 | baz buzz'''); 22 | } 23 | } 24 | ``` 25 | 26 | See the [example](./example/lib/main.dart) for more. 27 | 28 | ## Rich text 29 | 30 | Use Org markup to create rich `Text`-equivalent widgets with `OrgText`. 31 | 32 | ```dart 33 | OrgText('*This* is a /rich/ text label ([[https://example.com][details]])') 34 | ``` 35 | 36 | ## Advanced 37 | 38 | For more advanced usage, such as specifying link handling, use `OrgController` 39 | in concert with `OrgRootWidget`: 40 | 41 | ```dart 42 | import 'package:org_flutter/org_flutter.dart'; 43 | 44 | Widget build(BuildContext context) { 45 | final doc = OrgDocument.parse( 46 | rawOrgModeDocString, 47 | // Interpret e.g. #+TODO: settings at the cost of a second parsing pass 48 | interpretEmbeddedSettings: true, 49 | ); 50 | return OrgController( 51 | root: doc, 52 | child: OrgLocator( // Include OrgLocator to enable tap-to-jump on footnotes, etc. 53 | child: OrgRootWidget( 54 | style: myTextStyle, 55 | onLinkTap: launch, // e.g. from url_launcher package 56 | child: OrgDocumentWidget(doc), 57 | ), 58 | ), 59 | ); 60 | } 61 | ``` 62 | 63 | Place `OrgController` higher up in your widget hierarchy and access via 64 | `OrgController.of(context)` to dynamically control various properties of the 65 | displayed document: 66 | 67 | ```dart 68 | IconButton( 69 | icon: const Icon(Icons.repeat), 70 | onPressed: OrgController.of(context).cycleVisibility, 71 | ); 72 | ``` 73 | 74 | ## Text selection 75 | 76 | The Org Mode text is not selectable by default, but you can make it so by 77 | wrapping the widget in 78 | [`SelectionArea`](https://api.flutter.dev/flutter/material/SelectionArea-class.html). 79 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | strict-inference: true 7 | strict-raw-types: true 8 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Exceptions to above rules. 44 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 45 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 2738a1148ba6c9a6114df62358109407c3ef2553 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | An example demonstrating the org_flutter package 4 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | namespace 'com.example.example' 30 | 31 | compileSdk flutter.compileSdkVersion 32 | 33 | sourceSets { 34 | main.java.srcDirs += 'src/main/kotlin' 35 | } 36 | 37 | 38 | defaultConfig { 39 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 40 | applicationId "com.example.example" 41 | minSdkVersion flutter.minSdkVersion 42 | targetSdkVersion flutter.targetSdkVersion 43 | versionCode flutterVersionCode.toInteger() 44 | versionName flutterVersionName 45 | } 46 | 47 | buildTypes { 48 | release { 49 | // TODO: Add your own signing config for the release build. 50 | // Signing with the debug keys for now, so `flutter run --release` works. 51 | signingConfig signingConfigs.debug 52 | } 53 | } 54 | } 55 | 56 | flutter { 57 | source '../..' 58 | } 59 | 60 | dependencies { 61 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 62 | } 63 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity : FlutterActivity() 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.22' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 10 22:45:41 JST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 3 | #include "Generated.xcconfig" 4 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 3 | #include "Generated.xcconfig" 4 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_tex_js_ios (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - flutter_tex_js_ios (from `.symlinks/plugins/flutter_tex_js_ios/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | flutter_tex_js_ios: 14 | :path: ".symlinks/plugins/flutter_tex_js_ios/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 18 | flutter_tex_js_ios: babbfcf08c2586a67d7ee713dbb5e8acdff19dd6 19 | 20 | PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011 21 | 22 | COCOAPODS: 1.14.3 23 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | void main() { 5 | runApp(const MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | const MyApp({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return MaterialApp( 14 | title: 'org_flutter', 15 | restorationScopeId: 'example_root', 16 | theme: ThemeData( 17 | primarySwatch: Colors.blue, 18 | visualDensity: VisualDensity.adaptivePlatformDensity, 19 | ), 20 | home: const MyHomePage(), 21 | ); 22 | } 23 | } 24 | 25 | class MyHomePage extends StatelessWidget { 26 | const MyHomePage({super.key}); 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return DefaultTabController( 31 | length: 2, 32 | child: Scaffold( 33 | appBar: AppBar( 34 | title: const Text('org_flutter'), 35 | bottom: const TabBar( 36 | tabs: [ 37 | Tab(text: 'Simple'), 38 | Tab(text: 'Complex'), 39 | ], 40 | ), 41 | ), 42 | body: const TabBarView(children: [ 43 | SimpleTab(), 44 | ComplexTab(), 45 | ]), 46 | ), 47 | ); 48 | } 49 | } 50 | 51 | class SimpleTab extends StatelessWidget { 52 | const SimpleTab({super.key}); 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return const Org( 57 | '''* TODO [#A] foo bar 58 | baz buzz''', 59 | restorationId: 'my_org_widget', 60 | ); 61 | } 62 | } 63 | 64 | class ComplexTab extends StatefulWidget { 65 | const ComplexTab({super.key}); 66 | 67 | @override 68 | State createState() => _ComplexTabState(); 69 | } 70 | 71 | class _ComplexTabState extends State { 72 | late OrgDocument root; 73 | 74 | @override 75 | void initState() { 76 | root = OrgDocument.parse('''* TODO [#A] foo bar 77 | ~1~'''); 78 | 79 | super.initState(); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | return OrgController( 85 | root: root, 86 | child: ListView( 87 | children: [ 88 | OrgRootWidget(child: OrgDocumentWidget(root, shrinkWrap: true)), 89 | ElevatedButton( 90 | onPressed: _incrementCounter, 91 | child: const Text('Increment'), 92 | ), 93 | ], 94 | ), 95 | ); 96 | } 97 | 98 | void _incrementCounter() { 99 | late OrgMarkup markupNode; 100 | root.visit((node) { 101 | markupNode = node; 102 | return false; // stop visiting 103 | }); 104 | final value = int.parse(markupNode.content.children.single.toMarkup()); 105 | setState(() { 106 | root = root 107 | .editNode(markupNode)! 108 | .replace(OrgMarkup.just('${value + 1}', OrgStyle.code)) 109 | .commit() as OrgDocument; 110 | }); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: An example app demonstrating the org_flutter package 3 | 4 | publish_to: 'none' 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=3.0.0 <4.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | org_flutter: 15 | path: .. 16 | 17 | dev_dependencies: 18 | flutter_lints: ^5.0.0 19 | flutter_test: 20 | sdk: flutter 21 | 22 | flutter: 23 | uses-material-design: true 24 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:example/main.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | testWidgets('Smoke test', (tester) async { 6 | await tester.pumpWidget(const MyApp()); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amake/org_flutter/ce28d119208cffe3bf913f558a4b4e215b2a71c9/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | example 18 | 19 | 20 | 21 | 24 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/error.dart: -------------------------------------------------------------------------------- 1 | typedef OrgErrorHandler = void Function(dynamic); 2 | 3 | sealed class OrgError implements Exception { 4 | const OrgError(this.message); 5 | 6 | final String message; 7 | } 8 | 9 | class OrgParserError extends OrgError { 10 | const OrgParserError(super.message, this.result); 11 | 12 | final dynamic result; 13 | } 14 | 15 | class OrgExecutionError extends OrgError { 16 | const OrgExecutionError(super.message, this.code, this.cause); 17 | 18 | final String code; 19 | final dynamic cause; 20 | } 21 | 22 | class OrgTimeoutError extends OrgError { 23 | const OrgTimeoutError(super.message, this.code, this.timeLimit); 24 | 25 | final String code; 26 | final Duration timeLimit; 27 | } 28 | 29 | class OrgArgumentError extends OrgError { 30 | const OrgArgumentError(super.message, this.item); 31 | 32 | final dynamic item; 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/events.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:org_flutter/src/controller.dart'; 3 | import 'package:org_parser/org_parser.dart'; 4 | 5 | /// A widget for managing callbacks invoked upon user interaction or other 6 | /// document-related events. 7 | class OrgEvents extends InheritedWidget { 8 | const OrgEvents({ 9 | required super.child, 10 | this.onLinkTap, 11 | this.onLocalSectionLinkTap, 12 | this.onSectionLongPress, 13 | this.onSectionSlide, 14 | this.onListItemTap, 15 | this.onCitationTap, 16 | this.onTimestampTap, 17 | this.loadImage, 18 | super.key, 19 | }); 20 | 21 | /// A callback invoked when the user taps a link. The argument is the 22 | /// [OrgLink] object; the URL is [OrgLink.location]. You might want to open 23 | /// this in a browser. 24 | final void Function(OrgLink)? onLinkTap; 25 | 26 | /// A callback invoked when the user taps on a link to a section within the 27 | /// current document. The argument is the target section. You might want to 28 | /// display it somehow. 29 | final void Function(OrgTree)? onLocalSectionLinkTap; 30 | 31 | /// A callback invoked when the user long-presses on a section headline within 32 | /// the current document. The argument is the pressed section. You might want 33 | /// to narrow the display to show just this section. 34 | final void Function(OrgSection)? onSectionLongPress; 35 | 36 | /// A callback invoked to build a list of actions revealed when the user 37 | /// slides a section. The argument is the section being slid. Consider 38 | /// supplying instances of `SlidableAction` from the 39 | /// [flutter_slidable](https://pub.dev/packages/flutter_slidable) package. 40 | final List Function(OrgSection)? onSectionSlide; 41 | 42 | /// A callback invoked when the user taps on a list item that has a checkbox 43 | /// within the current document. The argument is the tapped item. You might 44 | /// want to toggle the checkbox. 45 | final void Function(OrgListItem)? onListItemTap; 46 | 47 | /// A callback invoked when the user taps on a citation. 48 | final void Function(OrgCitation)? onCitationTap; 49 | 50 | /// A callback invoked when the user taps on a timestamp. 51 | final void Function(OrgNode)? onTimestampTap; 52 | 53 | /// A callback invoked when an image should be displayed. The argument is the 54 | /// [OrgLink] describing where the image data can be found. It is your 55 | /// responsibility to resolve the link, fetch the data, and return a widget 56 | /// for displaying the image. 57 | /// 58 | /// Return null instead to display the link text. 59 | final Widget? Function(OrgLink)? loadImage; 60 | 61 | static OrgEvents of(BuildContext context) => 62 | context.dependOnInheritedWidgetOfExactType()!; 63 | 64 | /// Invoke the appropriate handler for the given [url] 65 | void dispatchLinkTap(BuildContext context, OrgLink link) { 66 | final section = _resolveLocalSectionLink(context, link.location); 67 | if (section != null) { 68 | onLocalSectionLinkTap?.call(section); 69 | } else { 70 | onLinkTap?.call(link); 71 | } 72 | } 73 | 74 | OrgTree? _resolveLocalSectionLink(BuildContext context, String url) { 75 | if (isOrgLocalSectionUrl(url)) { 76 | final sectionTitle = parseOrgLocalSectionUrl(url); 77 | final section = OrgController.of(context).sectionWithTitle(sectionTitle); 78 | if (section == null) { 79 | debugPrint('Failed to find local section with title "$sectionTitle"'); 80 | } 81 | return section; 82 | } else if (isOrgIdUrl(url)) { 83 | final sectionId = parseOrgIdUrl(url); 84 | final section = OrgController.of(context).sectionWithId(sectionId); 85 | if (section == null) { 86 | debugPrint('Failed to find local section with ID "$sectionId"'); 87 | } 88 | return section; 89 | } else if (isOrgCustomIdUrl(url)) { 90 | final sectionId = parseOrgCustomIdUrl(url); 91 | final section = OrgController.of(context).sectionWithCustomId(sectionId); 92 | if (section == null) { 93 | debugPrint('Failed to find local section with CUSTOM_ID "$sectionId"'); 94 | } 95 | return section; 96 | } 97 | try { 98 | final link = OrgFileLink.parse(url); 99 | if (link.isLocal) { 100 | return _resolveLocalSectionLink(context, link.extra!); 101 | } 102 | } on Exception { 103 | // Ignore 104 | } 105 | return null; 106 | } 107 | 108 | @override 109 | bool updateShouldNotify(OrgEvents oldWidget) => 110 | onLinkTap != oldWidget.onLinkTap || 111 | onSectionLongPress != oldWidget.onSectionLongPress; 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/flash.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | class AnimatedTextFlash extends StatefulWidget { 5 | const AnimatedTextFlash({ 6 | required this.child, 7 | required this.cookie, 8 | this.times = 2, 9 | super.key, 10 | }); 11 | 12 | final Widget child; 13 | final int times; 14 | final dynamic cookie; 15 | 16 | @override 17 | State createState() => _AnimatedTextFlashState(); 18 | } 19 | 20 | class _AnimatedTextFlashState extends State 21 | with SingleTickerProviderStateMixin { 22 | late AnimationController _animation; 23 | 24 | @override 25 | void initState() { 26 | _animation = AnimationController( 27 | vsync: this, 28 | duration: const Duration(milliseconds: 100), 29 | ); 30 | super.initState(); 31 | } 32 | 33 | @override 34 | void dispose() { 35 | _animation.dispose(); 36 | super.dispose(); 37 | } 38 | 39 | @override 40 | void didUpdateWidget(covariant AnimatedTextFlash oldWidget) { 41 | if (oldWidget.cookie != widget.cookie) { 42 | _flash(); 43 | } 44 | super.didUpdateWidget(oldWidget); 45 | } 46 | 47 | void _flash() async { 48 | for (var i = 0; i < widget.times; i++) { 49 | await _animation.forward(); 50 | await _animation.reverse(); 51 | } 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | final defaultStyle = DefaultTextStyle.of(context).style; 57 | 58 | return DefaultTextStyleTransition( 59 | style: _animation.drive( 60 | TextStyleTween( 61 | begin: defaultStyle, 62 | end: defaultStyle.copyWith( 63 | backgroundColor: OrgTheme.dataOf(context).highlightColor), 64 | ).chain( 65 | CurveTween(curve: Curves.linearToEaseOut), 66 | ), 67 | ), 68 | child: widget.child, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/folding.dart: -------------------------------------------------------------------------------- 1 | enum OrgVisibilityState { 2 | /// Just the root headline; equivalent to global "overview" state 3 | folded, 4 | 5 | /// All headlines of all levels 6 | contents, 7 | 8 | /// All immediate children (subtrees folded) 9 | children, 10 | 11 | /// Everything 12 | subtree, 13 | 14 | /// Not shown; for use in sparse trees 15 | hidden, 16 | } 17 | 18 | extension OrgVisibilityStateJson on OrgVisibilityState? { 19 | String? toJson() => this?.toString(); 20 | 21 | static OrgVisibilityState? fromJson(String? json) => json == null 22 | ? null 23 | : OrgVisibilityState.values.singleWhere( 24 | (value) => value.toString() == json, 25 | ); 26 | } 27 | 28 | extension OrgVisibilityStateCycling on OrgVisibilityState { 29 | OrgVisibilityState get cycleGlobal { 30 | switch (this) { 31 | case OrgVisibilityState.folded: 32 | return OrgVisibilityState.contents; 33 | case OrgVisibilityState.contents: 34 | return OrgVisibilityState.subtree; 35 | case OrgVisibilityState.subtree: 36 | case OrgVisibilityState.children: 37 | return OrgVisibilityState.folded; 38 | case OrgVisibilityState.hidden: 39 | return OrgVisibilityState.hidden; 40 | } 41 | } 42 | 43 | OrgVisibilityState cycleSubtree(bool empty) { 44 | switch (this) { 45 | case OrgVisibilityState.folded: 46 | return OrgVisibilityState.children; 47 | case OrgVisibilityState.contents: 48 | return empty ? OrgVisibilityState.subtree : OrgVisibilityState.folded; 49 | case OrgVisibilityState.children: 50 | return empty ? OrgVisibilityState.folded : OrgVisibilityState.subtree; 51 | case OrgVisibilityState.subtree: 52 | return OrgVisibilityState.folded; 53 | case OrgVisibilityState.hidden: 54 | return OrgVisibilityState.hidden; 55 | } 56 | } 57 | 58 | OrgVisibilityState get subtreeState { 59 | switch (this) { 60 | case OrgVisibilityState.folded: // fallthrough 61 | case OrgVisibilityState.contents: // fallthrough 62 | case OrgVisibilityState.children: 63 | return OrgVisibilityState.folded; 64 | case OrgVisibilityState.subtree: 65 | return OrgVisibilityState.subtree; 66 | case OrgVisibilityState.hidden: 67 | return OrgVisibilityState.hidden; 68 | } 69 | } 70 | } 71 | 72 | typedef OrgVisibilityResult = ({bool? searchHit, bool? sparseHit}); 73 | 74 | extension OrgVisibilityResultUtil on OrgVisibilityResult { 75 | OrgVisibilityResult or(OrgVisibilityResult other) => ( 76 | searchHit: searchHit == null ? null : searchHit! || other.searchHit!, 77 | sparseHit: sparseHit == null ? null : sparseHit! || other.sparseHit!, 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/highlight.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:highlighting/highlighting.dart'; 4 | import 'package:highlighting/languages/all.dart'; 5 | import 'package:org_flutter/src/widgets.dart'; 6 | 7 | bool supportedSrcLanguage(String? language) => 8 | allLanguages.containsKey(language) || 9 | allLanguages.containsKey(language?.toLowerCase()); 10 | 11 | Widget buildSrcHighlight( 12 | BuildContext context, { 13 | required String code, 14 | required String? languageId, 15 | }) => 16 | HighlightView( 17 | code, 18 | theme: OrgTheme.dataOf(context).srcTheme ?? {}, 19 | languageId: languageId, 20 | textStyle: DefaultTextStyle.of(context).style, 21 | ); 22 | 23 | TextSpan buildSrcHighlightSpan( 24 | BuildContext context, { 25 | required String code, 26 | required String? languageId, 27 | }) => 28 | _highlightedSpan( 29 | code, 30 | languageId: languageId, 31 | theme: OrgTheme.dataOf(context).srcTheme ?? {}, 32 | textStyle: DefaultTextStyle.of(context).style, 33 | ); 34 | 35 | // Below copied from: 36 | // https://github.com/akvelon/dart-highlighting/blob/25bc512c66d9eead9012dd129d0a12e77393b828/flutter_highlighting/lib/flutter_highlighting.dart 37 | // 38 | // with the following changes: 39 | // 40 | // - Replace `RichText` with `Text.rich`; see 41 | // https://github.com/akvelon/dart-highlighting/pull/71 42 | // - Fix lints 43 | // - Refactor to allow obtaining just the `TextSpan` 44 | 45 | const _rootKey = 'root'; 46 | const _defaultFontColor = Color(0xff000000); 47 | const _defaultBackgroundColor = Color(0xffffffff); 48 | 49 | // TODO: dart:io is not available at web platform currently 50 | // See: https://github.com/flutter/flutter/issues/39998 51 | // So we just use monospace here for now 52 | const _defaultFontFamily = 'monospace'; 53 | 54 | /// Highlight Flutter Widget 55 | class HighlightView extends StatelessWidget { 56 | /// The original code to be highlighted 57 | final String source; 58 | 59 | /// Highlight language 60 | /// 61 | /// It is recommended to give it a value for performance 62 | /// 63 | /// [All available languages](https://github.com/akvelon/dart-highlighting/tree/main/highlighting/lib/languages) 64 | final String? languageId; 65 | 66 | /// Highlight theme 67 | /// 68 | /// [All available themes](https://github.com/akvelon/dart-highlighting/tree/main/flutter_highlighting/lib/themes) 69 | final Map theme; 70 | 71 | /// Padding 72 | final EdgeInsetsGeometry? padding; 73 | 74 | /// Text styles 75 | /// 76 | /// Specify text styles such as font family and font size 77 | final TextStyle? textStyle; 78 | 79 | HighlightView( 80 | String input, { 81 | this.languageId, 82 | this.theme = const {}, 83 | this.padding, 84 | this.textStyle, 85 | int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 86 | super.key, 87 | }) : source = input.replaceAll('\t', ' ' * tabSize); 88 | 89 | @override 90 | Widget build(BuildContext context) { 91 | return Container( 92 | color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor, 93 | padding: padding, 94 | child: Text.rich( 95 | _highlightedSpan( 96 | source, 97 | languageId: languageId, 98 | theme: theme, 99 | textStyle: textStyle, 100 | ), 101 | ), 102 | ); 103 | } 104 | } 105 | 106 | TextSpan _highlightedSpan( 107 | String source, { 108 | String? languageId, 109 | Map theme = const {}, 110 | TextStyle? textStyle, 111 | }) { 112 | var style = TextStyle( 113 | fontFamily: _defaultFontFamily, 114 | color: theme[_rootKey]?.color ?? _defaultFontColor, 115 | ); 116 | if (textStyle != null) { 117 | style = style.merge(textStyle); 118 | } 119 | 120 | return TextSpan( 121 | style: style, 122 | children: _convert( 123 | // ignore: invalid_use_of_internal_member 124 | highlight.highlight(languageId ?? '', source, true).nodes ?? [], 125 | theme, 126 | ), 127 | ); 128 | } 129 | 130 | List _convert(List nodes, Map theme) { 131 | List spans = []; 132 | var currentSpans = spans; 133 | List> stack = []; 134 | 135 | traverse(Node node) { 136 | if (node.value != null) { 137 | currentSpans.add(node.className == null 138 | ? TextSpan(text: node.value) 139 | : TextSpan(text: node.value, style: theme[node.className])); 140 | } else { 141 | List tmp = []; 142 | currentSpans.add(TextSpan(children: tmp, style: theme[node.className])); 143 | stack.add(currentSpans); 144 | currentSpans = tmp; 145 | 146 | for (var n in node.children) { 147 | traverse(n); 148 | if (n == node.children.last) { 149 | currentSpans = stack.isEmpty ? spans : stack.removeLast(); 150 | } 151 | } 152 | } 153 | } 154 | 155 | for (var node in nodes) { 156 | traverse(node); 157 | } 158 | 159 | return spans; 160 | } 161 | -------------------------------------------------------------------------------- /lib/src/indent.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class IndentContext extends InheritedWidget { 4 | const IndentContext(this.indentSize, {required super.child, super.key}); 5 | 6 | final int indentSize; 7 | 8 | @override 9 | bool updateShouldNotify(IndentContext oldWidget) => 10 | indentSize != oldWidget.indentSize; 11 | 12 | static IndentContext? of(BuildContext context) => 13 | context.dependOnInheritedWidgetOfExactType(); 14 | } 15 | 16 | class IndentBuilder extends StatelessWidget { 17 | const IndentBuilder( 18 | this.indent, { 19 | this.expanded = true, 20 | required this.builder, 21 | super.key, 22 | }); 23 | 24 | final Widget Function(BuildContext, int) builder; 25 | final String indent; 26 | final bool expanded; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | final parentIndent = IndentContext.of(context)?.indentSize ?? 0; 31 | final newIndent = 32 | indent.length >= parentIndent ? indent.substring(parentIndent) : ''; 33 | final totalIndentSize = parentIndent + newIndent.length; 34 | Widget child = IndentContext( 35 | parentIndent + newIndent.length, 36 | child: builder(context, totalIndentSize), 37 | ); 38 | if (expanded) { 39 | child = Expanded(child: child); 40 | } 41 | return Row( 42 | crossAxisAlignment: CrossAxisAlignment.start, 43 | children: [Text(newIndent), child], 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/locatable.dart: -------------------------------------------------------------------------------- 1 | export 'widget/org_footnote_reference.dart' show FootnoteKey; 2 | export 'widget/org_link_target.dart' show LinkTargetKey; 3 | export 'widget/org_meta.dart' show NameKey; 4 | export 'widget/org_radio_target.dart' show RadioTargetKey; 5 | -------------------------------------------------------------------------------- /lib/src/search.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/controller.dart'; 3 | 4 | typedef SearchResultKey = GlobalKey; 5 | 6 | class SearchResult extends StatefulWidget { 7 | static Widget of(BuildContext context, {required Widget child}) => 8 | SearchResult( 9 | key: OrgController.of(context).generateSearchResultKey(), 10 | child: child, 11 | ); 12 | 13 | const SearchResult({required this.child, super.key}); 14 | final Widget child; 15 | 16 | @override 17 | State createState() => SearchResultState(); 18 | } 19 | 20 | /// The state object for a search result. Consumers of 21 | /// [OrgControllerData.searchResultKeys] can use [selected] to toggle focus 22 | /// highlighting. 23 | class SearchResultState extends State { 24 | bool _selected = false; 25 | late OrgControllerData _controller; 26 | 27 | @override 28 | void didChangeDependencies() { 29 | super.didChangeDependencies(); 30 | _controller = OrgController.of(context); 31 | } 32 | 33 | bool get selected => _selected; 34 | 35 | set selected(bool value) { 36 | setState(() => _selected = value); 37 | } 38 | 39 | @override 40 | void dispose() { 41 | super.dispose(); 42 | final key = widget.key; 43 | if (key is SearchResultKey) { 44 | _controller.removeSearchResultKey(key); 45 | } 46 | } 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return _selected 51 | ? DecoratedBox( 52 | decoration: BoxDecoration( 53 | border: Border.all( 54 | color: Theme.of(context).colorScheme.outline, 55 | width: 0.5, 56 | ), 57 | ), 58 | position: DecorationPosition.foreground, 59 | child: widget.child, 60 | ) 61 | : widget.child; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/util/alignment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_parser/org_parser.dart'; 3 | 4 | enum OrgAlignment { left, center, right } 5 | 6 | final _attrPattern = RegExp(r'^#\+attr(.*):$', caseSensitive: false); 7 | 8 | OrgAlignment? alignmentForNode(OrgNode node, OrgTree root) { 9 | // TODO(aaron): This is potentially expensive. Think of a better way to track 10 | // the path within the tree. 11 | var zipper = root.editNode(node); 12 | if (zipper == null) return null; 13 | 14 | // For the document 15 | // 16 | // ``` 17 | // #+ATTR_ORG: :align center 18 | // [[foo]] 19 | // ``` 20 | // 21 | // the tree will look like: 22 | // 23 | // OrgDocument: #+ATTR_ORG... 24 | // OrgContent: #+ATTR_ORG... 25 | // OrgMeta: #+ATTR_ORG... 26 | // OrgContent: :align ce... 27 | // OrgPlainText: :align ce... 28 | // OrgParagraph: [[foo]]\n 29 | // OrgContent: [[foo]]\n 30 | // OrgLink: [[foo]] 31 | // OrgPlainText: "\n" 32 | // 33 | // Instead of going up a fixed number of times, we go up until we can go left. 34 | // This is important because we don't want to detect alignment for things 35 | // inline within a paragraph. 36 | // 37 | // - Alignable item: alone in an OrgParagraph; must go up to the level that 38 | // might have an OrgMeta sibling 39 | // 40 | // - Non-alignable item: will have siblings within OrgParagraph; will look at those 41 | // and fail to find an OrgMeta (which is good) 42 | while (!zipper!.canGoLeft()) { 43 | if (!zipper.canGoUp()) return null; 44 | zipper = zipper.goUp(); 45 | } 46 | 47 | OrgAlignment? result; 48 | while (zipper!.canGoLeft()) { 49 | zipper = zipper.goLeft(); 50 | final node = zipper.node; 51 | if (node is! OrgMeta) continue; 52 | 53 | final match = _attrPattern.firstMatch(node.key); 54 | if (match == null) continue; 55 | final authoritative = match.group(1)?.toLowerCase() == '_org'; 56 | 57 | final value = node.value?.toMarkup(); 58 | if (value == null) continue; 59 | 60 | final plist = tokenizePlist(value); 61 | final center = plist.get(':center'); 62 | if (center == 't') { 63 | result = OrgAlignment.center; 64 | if (authoritative) break; 65 | } else { 66 | final alignment = plist.get(':align'); 67 | if (alignment == null) continue; 68 | switch (alignment.toLowerCase()) { 69 | case 'left': 70 | result = OrgAlignment.left; 71 | case 'center': 72 | result = OrgAlignment.center; 73 | case 'right': 74 | result = OrgAlignment.right; 75 | } 76 | if (authoritative) break; 77 | } 78 | } 79 | return result; 80 | } 81 | 82 | typedef Plist = List; 83 | 84 | // TODO(aaron): Handle this properly, like with support for quoted strings. 85 | Plist tokenizePlist(String plist) => plist 86 | .split(RegExp(r'\s+')) 87 | .map((s) => s.trim()) 88 | .where((s) => s.isNotEmpty) 89 | .toList(growable: false); 90 | 91 | extension PlistExtension on Plist { 92 | String? get(String key) { 93 | for (var i = 0; i < length; i++) { 94 | final token = this[i]; 95 | if (token.toLowerCase() == key) { 96 | if (i + 1 < length) { 97 | return this[i + 1]; 98 | } 99 | } 100 | } 101 | return null; 102 | } 103 | } 104 | 105 | extension OrgAlignmentExtension on OrgAlignment { 106 | MainAxisAlignment get toMainAxisAlignment => switch (this) { 107 | OrgAlignment.left => MainAxisAlignment.start, 108 | OrgAlignment.center => MainAxisAlignment.center, 109 | OrgAlignment.right => MainAxisAlignment.end 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/util/bidi.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:more/char_matcher.dart'; 3 | import 'package:org_flutter/org_flutter.dart'; 4 | 5 | extension BidiUtil on OrgNode { 6 | TextDirection? detectTextDirection() { 7 | final serializer = OrgBidiDetectionSerializer(); 8 | toMarkup(serializer: serializer); 9 | if (serializer.strongChar == null) return null; 10 | return UnicodeCharMatcher.bidiLeftToRight().match(serializer.strongChar!) 11 | ? TextDirection.ltr 12 | : TextDirection.rtl; 13 | } 14 | } 15 | 16 | class OrgBidiDetectionSerializer extends OrgSerializer { 17 | int? strongChar; 18 | bool canceled = false; 19 | 20 | @override 21 | void visit(OrgNode node) { 22 | if (canceled) return; 23 | super.visit(node); 24 | } 25 | 26 | @override 27 | void write(String str) { 28 | if (canceled) return; 29 | 30 | final idx = UnicodeCharMatcher.bidiStrong().firstIndexIn(str); 31 | if (idx != -1) { 32 | canceled = true; 33 | strongChar = str.codeUnitAt(idx); 34 | } 35 | super.write(str); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/util/collection.dart: -------------------------------------------------------------------------------- 1 | Iterable interleave(Iterable items, T withItem) sync* { 2 | for (final item in items) { 3 | yield item; 4 | yield withItem; 5 | } 6 | } 7 | 8 | Iterable zipMap( 9 | Iterable a, Iterable b, R Function(T, U) visit) sync* { 10 | final iterA = a.iterator; 11 | final iterB = b.iterator; 12 | while (iterA.moveNext() && iterB.moveNext()) { 13 | yield visit(iterA.current, iterB.current); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/util/elisp.dart: -------------------------------------------------------------------------------- 1 | import 'dart:developer'; 2 | 3 | import 'package:petit_lisp/lisp.dart'; 4 | import 'package:petitparser/petitparser.dart'; 5 | 6 | class ElispParserDefinition extends LispParserDefinition { 7 | @override 8 | Parser atomChoice() => super.atomChoice() 9 | // # can start a symbol, so put functionQuote before symbol 10 | ..replace(ref0(symbol), ref0(functionQuote) | ref0(symbol)); 11 | 12 | Parser functionQuote() => 13 | (string("#'") & ref0(atom)).map((each) => Cons.quote(each[1])); 14 | } 15 | 16 | final _definition = ElispParserDefinition(); 17 | final elispParser = _definition.build(); 18 | 19 | // TODO(aaron): more accurate standard env 20 | class ElispEnvironment extends Environment { 21 | ElispEnvironment(super.owner) { 22 | // petit_lisp's `set!` does not evaluate the symbol 23 | define(Name('set'), _set); 24 | define(Name('setq'), _setq); 25 | define(Name('debugger'), _debugger); 26 | evalString(elispParser, this, _standardLibrary); 27 | } 28 | 29 | static dynamic _set(Environment env, dynamic args) { 30 | final sym = eval(env, args.head); 31 | if (sym is! Name) { 32 | throw ArgumentError('set: first argument must be a symbol'); 33 | } 34 | return env[sym] = eval(env, args.tail.head); 35 | } 36 | 37 | static dynamic _setq(Environment env, dynamic args) { 38 | dynamic result; 39 | while (args is Cons) { 40 | final sym = args.head; 41 | if (sym is! Name) { 42 | throw ArgumentError('Invalid setq: $sym is not a symbol'); 43 | } 44 | args = args.tail; 45 | result = env.setOrDefine(sym, eval(env, args.head)); 46 | args = args.tail; 47 | } 48 | return result; 49 | } 50 | 51 | static dynamic _debugger(Environment env, dynamic args) { 52 | debugger(); 53 | } 54 | 55 | static const _standardLibrary = ''' 56 | (define t true) 57 | (define nil null) 58 | 59 | (define equal =) 60 | (define eq eq?) 61 | 62 | (define lambda lambda*) 63 | 64 | (define-macro* (defun name args . body) 65 | `(define* ,(cons name args) ,@body)) 66 | 67 | (define-macro (defvar name value) 68 | `(define ,name ,value)) 69 | 70 | (define-macro* (defmacro name args . body) 71 | `(define-macro* ,(cons name args) ,@body)) 72 | 73 | (defun add-to-list (list-var element &optional appendp compare-fn) 74 | (if (not (member element (eval list-var) (eval compare-fn))) 75 | (set list-var (if appendp 76 | (append (eval list-var) (cons element nil)) 77 | (cons element (eval list-var))))) 78 | (eval list-var)) 79 | 80 | (defmacro dolist (spec &rest body) 81 | (let ((var (car spec)) 82 | (templist (make-symbol "list")) 83 | (resultvar (caddr spec))) 84 | `(let ((,templist ,(cadr spec)) 85 | ,var 86 | ,@(when resultvar `(,resultvar))) 87 | (while ,templist 88 | (setq ,var (car ,templist)) 89 | ,@body 90 | (setq ,templist (cdr ,templist))) 91 | ,resultvar))) 92 | '''; 93 | } 94 | 95 | extension ElispExt on Environment { 96 | Environment get _root { 97 | var env = this; 98 | while (env.owner != null) { 99 | env = env.owner!; 100 | } 101 | return env; 102 | } 103 | 104 | dynamic setOrDefine(Name key, dynamic value) { 105 | try { 106 | return this[key] = value; 107 | } on ArgumentError { 108 | return _root.define(key, value); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/util/keywords.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_flutter/org_flutter.dart'; 2 | 3 | List getStartupSettings(OrgTree tree) { 4 | final result = []; 5 | tree.visit((meta) { 6 | if (meta.key.toUpperCase() == '#+STARTUP:' && meta.value != null) { 7 | for (final setting in meta.value!.toMarkup().trim().split(' ')) { 8 | result.add(setting.toLowerCase()); 9 | } 10 | } 11 | return true; 12 | }); 13 | return result; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/util/locale.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | /// Extracts the locale from `#+LANGUAGE:` in the given [tree]. 5 | Locale? extractLocale( 6 | OrgTree tree, 7 | ) { 8 | Locale? result; 9 | tree.visit((meta) { 10 | if (meta.key.toUpperCase() == '#+LANGUAGE:' && meta.value != null) { 11 | final trailing = meta.value!.toMarkup().trim(); 12 | result = tryParseLocale(trailing); 13 | if (result != null) return false; 14 | } 15 | return true; 16 | }); 17 | debugPrint('Detected locale: $result'); 18 | return result; 19 | } 20 | 21 | Locale? tryParseLocale(String locale) { 22 | final parts = locale.split(RegExp('[_-]')); 23 | if (parts.length == 1 && parts[0].isNotEmpty) { 24 | return Locale(parts[0]); 25 | } else if (parts.length == 2 && parts[1].length == 2) { 26 | return Locale(parts[0], parts[1]); 27 | } else if (parts.length == 2 && parts[1].length == 4) { 28 | return Locale.fromSubtags( 29 | languageCode: parts[0], 30 | scriptCode: parts[1], 31 | ); 32 | } else if (parts.length == 3) { 33 | return Locale.fromSubtags( 34 | languageCode: parts[0], 35 | scriptCode: parts[1], 36 | countryCode: parts[2], 37 | ); 38 | } 39 | return null; 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/util/text.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:org_flutter/src/util/collection.dart'; 4 | 5 | /// If necessary, interleave runes with U+200B ZERO WIDTH SPACE to serve as a 6 | /// place to wrap the line. 7 | String characterWrappable(String text) { 8 | if (text.contains(' ')) { 9 | return text; 10 | } else { 11 | return String.fromCharCodes(interleave(text.runes, 0x200b)); 12 | } 13 | } 14 | 15 | extension PatternUtil on Pattern? { 16 | bool get isEmpty { 17 | final self = this; 18 | if (self is String) { 19 | return self.isEmpty; 20 | } else if (self is RegExp) { 21 | return self.pattern.isEmpty; 22 | } else { 23 | return self == null; 24 | } 25 | } 26 | 27 | bool sameAs(Pattern? other) { 28 | final self = this; 29 | if (self == other) { 30 | return true; 31 | } 32 | if (self is RegExp && other is RegExp) { 33 | return self.pattern == other.pattern && 34 | self.isCaseSensitive == other.isCaseSensitive && 35 | self.isDotAll == other.isDotAll && 36 | self.isMultiLine == other.isMultiLine && 37 | self.isUnicode == other.isUnicode; 38 | } 39 | return false; 40 | } 41 | } 42 | 43 | enum TokenLocation { start, middle, end, only } 44 | 45 | TokenLocation locationOf(Object elem, List elems) { 46 | final isLast = identical(elem, elems.last); 47 | final isFirst = identical(elem, elems.first); 48 | return isLast && isFirst 49 | ? TokenLocation.only 50 | : isLast 51 | ? TokenLocation.end 52 | : isFirst 53 | ? TokenLocation.start 54 | : TokenLocation.middle; 55 | } 56 | 57 | String reflowText(String text, TokenLocation location) => text.replaceAll( 58 | switch (location) { 59 | TokenLocation.only => _unwrappableWhitespacePattern, 60 | TokenLocation.start => _unwrappableStartWhitespacePattern, 61 | TokenLocation.middle => _unwrappableMiddleWhitespacePattern, 62 | TokenLocation.end => _unwrappableEndWhitespacePattern, 63 | }, 64 | ' ', 65 | ); 66 | 67 | // Match single (CR)LF between non-whitespace chars only (preserve leading and 68 | // trailing linebreaks) 69 | final _unwrappableWhitespacePattern = RegExp(r'(?<=\S)[ \t]*\r?\n[ \t]*(?=\S)'); 70 | // Match single (CR)LF between non-whitespace chars OR at end of text for 71 | // leading text run (preserve leading linebreaks) 72 | final _unwrappableStartWhitespacePattern = 73 | RegExp(r'(?<=\S)[ \t]*\r?\n[ \t]*(?=\S|$)'); 74 | // Match single (CR)LF between non-whitespace chars OR at edge of text for 75 | // "inside" text runs (preserve none) 76 | final _unwrappableMiddleWhitespacePattern = 77 | RegExp(r'(?<=\S|^)[ \t]*\r?\n[ \t]*(?=\S|$)'); 78 | // Match single (CR)LF between non-whitespace chars OR at start of text for 79 | // final text run (preserve trailing linebreaks) 80 | final _unwrappableEndWhitespacePattern = 81 | RegExp(r'(?<=\S|^)[ \t]*\r?\n[ \t]*(?=\S)'); 82 | 83 | String removeTrailingLineBreak(String text) { 84 | if (text.endsWith('\n')) { 85 | if (text.endsWith('\r\n')) { 86 | return text.substring(0, text.length - 2); 87 | } else { 88 | return text.substring(0, text.length - 1); 89 | } 90 | } else { 91 | return text; 92 | } 93 | } 94 | 95 | // Remove [deindentSize] spaces from the start of each line. If a line doesn't 96 | // have that many spaces, it is left unchanged. 97 | String hardDeindent(String text, int deindentSize) => 98 | text.replaceAll(_deindentPattern(deindentSize), ''); 99 | 100 | // Remove at spaces from the start of each line. The amount removed is the 101 | // smaller of [deindentSize] and the current indent of the entire text. 102 | String softDeindent(String text, int deindentSize) { 103 | if (deindentSize == 0) return text; 104 | final currentIndent = detectIndent(text); 105 | final effectiveDeindentSize = min(currentIndent, deindentSize); 106 | return text.replaceAll(_deindentPattern(effectiveDeindentSize), ''); 107 | } 108 | 109 | int detectIndent(String text) { 110 | var result = -1; 111 | for (var i = 0; i >= 0 && i < text.length;) { 112 | var indent = 0; 113 | while (i < text.length) { 114 | final c = text.codeUnitAt(i); 115 | if (c == 0x20) { 116 | indent++; 117 | } else { 118 | // Blank line doesn't count as indent. 119 | if (c == 0x0A) indent = -1; 120 | break; 121 | } 122 | if (++i == text.length) { 123 | // End of text doesn't count as indent. 124 | indent = -1; 125 | break; 126 | } 127 | } 128 | if (indent != -1) { 129 | if (result == -1 || indent < result) result = indent; 130 | } 131 | final next = i = text.indexOf('\n', i); 132 | if (next == -1) { 133 | break; 134 | } else { 135 | i = next + 1; 136 | } 137 | } 138 | return result == -1 ? 0 : result; 139 | } 140 | 141 | Pattern Function(int) _deindentPattern = _memoize1((indentSize) => RegExp( 142 | '^ {$indentSize}', 143 | multiLine: true, 144 | )); 145 | 146 | R Function(T) _memoize1(R Function(T) func) { 147 | final cache = {}; 148 | return (arg) => cache.putIfAbsent(arg, () => func(arg)); 149 | } 150 | 151 | bool looksLikeUrl(String text) => _urlLikeRegexp.hasMatch(text); 152 | 153 | final _urlLikeRegexp = RegExp(r'^\w+://'); 154 | 155 | bool looksLikeImagePath(String text) => _imagePathLikeRegexp.hasMatch(text); 156 | 157 | final _imagePathLikeRegexp = 158 | RegExp(r'\.(?:jpe?g|png|gif|webp|w?bmp|svg|avif)$'); 159 | 160 | String trimPrefSuff(String str, String prefix, String suffix) { 161 | if (str.startsWith(prefix) && str.endsWith(suffix)) { 162 | return str.substring(prefix.length, str.length - suffix.length); 163 | } 164 | return str; 165 | } 166 | -------------------------------------------------------------------------------- /lib/src/util/util.dart: -------------------------------------------------------------------------------- 1 | export './alignment.dart'; 2 | export './bidi.dart'; 3 | export './collection.dart'; 4 | export './keywords.dart'; 5 | export './local_variables.dart'; 6 | export './locale.dart'; 7 | export './text.dart'; 8 | export './value_notifier.dart'; 9 | export './widget.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/util/value_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | class SafeValueNotifier extends ValueNotifier { 6 | bool _disposed = false; 7 | 8 | SafeValueNotifier(super.value); 9 | 10 | bool get disposed => _disposed; 11 | 12 | @override 13 | void dispose() { 14 | super.dispose(); 15 | _disposed = true; 16 | } 17 | } 18 | 19 | extension ValueNotifierUtil on ValueNotifier { 20 | Future listenOnce(FutureOr Function() callback) { 21 | final result = Completer(); 22 | 23 | void listener() { 24 | result.complete(callback()); 25 | removeListener(listener); 26 | } 27 | 28 | addListener(listener); 29 | 30 | return result.future; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/util/widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | mixin OpenCloseable on State { 5 | late final ValueNotifier _openListenable; 6 | ValueNotifier get openListenable => _openListenable; 7 | 8 | bool get defaultOpen => true; 9 | 10 | @override 11 | void initState() { 12 | super.initState(); 13 | _openListenable = ValueNotifier(defaultOpen); 14 | } 15 | 16 | @override 17 | void dispose() { 18 | _openListenable.dispose(); 19 | super.dispose(); 20 | } 21 | } 22 | 23 | typedef RecognizerHandler = void Function(GestureRecognizer); 24 | 25 | mixin RecognizerManager on State { 26 | final _recognizers = []; 27 | 28 | @override 29 | void dispose() { 30 | for (final item in _recognizers) { 31 | item.dispose(); 32 | } 33 | super.dispose(); 34 | } 35 | 36 | void registerRecognizer(GestureRecognizer recognizer) => 37 | _recognizers.add(recognizer); 38 | } 39 | 40 | Widget listBottomSafeArea() => const SafeArea( 41 | top: false, 42 | left: false, 43 | right: false, 44 | child: SizedBox.shrink(), 45 | ); 46 | 47 | const _kReducedOpacity = 0.6; 48 | 49 | Widget reduceOpacity(Widget child, {bool enabled = true}) => 50 | enabled ? Opacity(opacity: _kReducedOpacity, child: child) : child; 51 | 52 | /// A utility for overriding the text scale to be 1 53 | class IdentityTextScale extends StatelessWidget { 54 | const IdentityTextScale({required this.child, super.key}); 55 | 56 | final Widget child; 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return MediaQuery( 61 | data: MediaQuery.of(context).copyWith( 62 | textScaler: const TextScaler.linear(1), 63 | ), 64 | child: child, 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/widget/org_block.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:org_flutter/src/highlight.dart'; 5 | import 'package:org_flutter/src/indent.dart'; 6 | import 'package:org_flutter/src/settings.dart'; 7 | import 'package:org_flutter/src/util/util.dart'; 8 | import 'package:org_flutter/src/widget/org_content.dart'; 9 | import 'package:org_flutter/src/widget/org_theme.dart'; 10 | import 'package:org_parser/org_parser.dart'; 11 | 12 | /// An Org Mode block 13 | class OrgBlockWidget extends StatefulWidget { 14 | const OrgBlockWidget(this.block, {super.key}); 15 | final OrgBlock block; 16 | 17 | @override 18 | State createState() => _OrgBlockWidgetState(); 19 | } 20 | 21 | class _OrgBlockWidgetState extends State 22 | with OpenCloseable { 23 | bool _inited = false; 24 | 25 | @override 26 | void didChangeDependencies() { 27 | super.didChangeDependencies(); 28 | if (!_inited) { 29 | openListenable.value = !OrgSettings.of(context).settings.hideBlockStartup; 30 | _inited = true; 31 | } 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final defaultStyle = DefaultTextStyle.of(context).style; 37 | final metaStyle = 38 | defaultStyle.copyWith(color: OrgTheme.dataOf(context).metaColor); 39 | final hideMarkup = OrgSettings.of(context).settings.deemphasizeMarkup; 40 | // Remove a line break because we introduce one by splitting the text into 41 | // two widgets in this Column 42 | final trailing = removeTrailingLineBreak(widget.block.trailing); 43 | return IndentBuilder( 44 | widget.block.indent, 45 | builder: (context, totalIndentSize) { 46 | return ValueListenableBuilder( 47 | valueListenable: openListenable, 48 | builder: (context, open, child) => Column( 49 | crossAxisAlignment: CrossAxisAlignment.stretch, 50 | children: [ 51 | _header(context, metaStyle, open: open, hideMarkup: hideMarkup), 52 | AnimatedSwitcher( 53 | duration: const Duration(milliseconds: 100), 54 | transitionBuilder: (child, animation) => 55 | SizeTransition(sizeFactor: animation, child: child), 56 | child: open ? child : const SizedBox.shrink(), 57 | ), 58 | if (trailing.isNotEmpty) 59 | // Remove another line break because the existence of even an 60 | // empty string here takes up a line. 61 | Text(removeTrailingLineBreak(trailing)), 62 | ], 63 | ), 64 | child: Column( 65 | crossAxisAlignment: CrossAxisAlignment.stretch, 66 | children: [ 67 | _body(context, totalIndentSize), 68 | reduceOpacity( 69 | Text( 70 | hardDeindent(widget.block.footer, totalIndentSize), 71 | style: metaStyle, 72 | ), 73 | enabled: hideMarkup, 74 | ), 75 | ], 76 | ), 77 | ); 78 | }, 79 | ); 80 | } 81 | 82 | Widget _header( 83 | BuildContext context, 84 | TextStyle metaStyle, { 85 | required bool hideMarkup, 86 | required bool open, 87 | }) { 88 | var text = widget.block.header.trimRight(); 89 | if (!hideMarkup && !open) { 90 | text += '...'; 91 | } 92 | Widget header = Text( 93 | text, 94 | style: metaStyle, 95 | softWrap: !hideMarkup, 96 | overflow: hideMarkup ? TextOverflow.fade : null, 97 | ); 98 | if (hideMarkup && !open) { 99 | header = Row( 100 | children: [ 101 | Flexible( 102 | child: header, 103 | ), 104 | if (hideMarkup && !open) Text('...', style: metaStyle) 105 | ], 106 | ); 107 | } 108 | header = reduceOpacity(header, enabled: hideMarkup); 109 | return InkWell( 110 | onTap: () => openListenable.value = !openListenable.value, 111 | child: header, 112 | ); 113 | } 114 | 115 | Widget _body(BuildContext context, int indentSize) { 116 | final block = widget.block; 117 | Widget body; 118 | if (block is OrgSrcBlock) { 119 | final codeNode = block.body as OrgPlainText; 120 | final code = 121 | removeTrailingLineBreak(softDeindent(codeNode.content, indentSize)); 122 | if (supportedSrcLanguage(block.language)) { 123 | body = buildSrcHighlight( 124 | context, 125 | code: code, 126 | languageId: block.language, 127 | ); 128 | } else { 129 | final defaultStyle = DefaultTextStyle.of(context).style; 130 | body = Text(code, 131 | style: defaultStyle.copyWith( 132 | color: OrgTheme.dataOf(context).codeColor)); 133 | } 134 | } else if (block.body is OrgPlainText) { 135 | final contentNode = block.body as OrgPlainText; 136 | final content = removeTrailingLineBreak( 137 | softDeindent(contentNode.content, indentSize)); 138 | body = Text(content); 139 | } else { 140 | // This feels a bit costly, but it's the easiest way to handle scenarios 141 | // where the body is indented *less* than the block delimiters. 142 | indentSize = min(indentSize, detectIndent(block.body.toMarkup())); 143 | body = OrgContentWidget( 144 | block.body, 145 | transformer: (elem, content) { 146 | final location = locationOf(elem, block.body.children!); 147 | var formattedContent = hardDeindent(content, indentSize); 148 | if (location == TokenLocation.end || location == TokenLocation.only) { 149 | formattedContent = removeTrailingLineBreak(formattedContent); 150 | } 151 | return formattedContent; 152 | }, 153 | ); 154 | } 155 | if (block.type == 'example' || block.type == 'export') { 156 | body = DefaultTextStyle( 157 | style: DefaultTextStyle.of(context).style.copyWith( 158 | color: OrgTheme.dataOf(context).codeColor, 159 | ), 160 | child: body, 161 | ); 162 | } else if (block.type == 'verse') { 163 | body = InheritedOrgSettings.merge( 164 | OrgSettings(reflowText: false), 165 | child: body, 166 | ); 167 | } 168 | return block.body is OrgContent 169 | ? body 170 | : SingleChildScrollView( 171 | scrollDirection: Axis.horizontal, 172 | physics: const AlwaysScrollableScrollPhysics(), 173 | child: body, 174 | ); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /lib/src/widget/org_comment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/settings.dart'; 3 | import 'package:org_flutter/src/span.dart'; 4 | import 'package:org_flutter/src/util/util.dart'; 5 | import 'package:org_flutter/src/widget/org_theme.dart'; 6 | import 'package:org_parser/org_parser.dart'; 7 | 8 | /// An Org comment 9 | class OrgCommentWidget extends StatelessWidget { 10 | const OrgCommentWidget(this.comment, {super.key}); 11 | final OrgComment comment; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final hideMarkup = OrgSettings.of(context).settings.deemphasizeMarkup; 16 | final body = SingleChildScrollView( 17 | scrollDirection: Axis.horizontal, 18 | child: FancySpanBuilder( 19 | builder: (context, spanBuilder) { 20 | final metaStyle = DefaultTextStyle.of(context) 21 | .style 22 | .copyWith(color: OrgTheme.dataOf(context).metaColor); 23 | return Text.rich( 24 | TextSpan(children: [ 25 | spanBuilder.highlightedSpan(comment.indent, style: metaStyle), 26 | spanBuilder.highlightedSpan(comment.start, style: metaStyle), 27 | spanBuilder.highlightedSpan( 28 | comment.content, 29 | style: metaStyle, 30 | ), 31 | spanBuilder.highlightedSpan( 32 | removeTrailingLineBreak(comment.trailing), 33 | style: metaStyle, 34 | ), 35 | ]), 36 | ); 37 | }, 38 | ), 39 | ); 40 | return reduceOpacity(body, enabled: hideMarkup); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/widget/org_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/span.dart'; 3 | import 'package:org_parser/org_parser.dart'; 4 | 5 | /// Generic Org Mode content 6 | class OrgContentWidget extends StatelessWidget { 7 | const OrgContentWidget( 8 | this.content, { 9 | this.transformer, 10 | this.textAlign, 11 | super.key, 12 | }); 13 | final OrgNode content; 14 | final Transformer? transformer; 15 | final TextAlign? textAlign; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | return FancySpanBuilder( 20 | builder: (context, spanBuilder) => Text.rich( 21 | spanBuilder.build( 22 | content, 23 | transformer: transformer ?? identityTransformer, 24 | ), 25 | textAlign: textAlign, 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/widget/org_decrypted_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/widget/org_content.dart'; 3 | import 'package:org_flutter/src/widget/org_section.dart'; 4 | import 'package:org_parser/org_parser.dart'; 5 | 6 | /// A widget representing content decrypted from an [OrgPgpBlock] 7 | class OrgDecryptedContentWidget extends StatelessWidget { 8 | const OrgDecryptedContentWidget(this.content, {super.key}); 9 | 10 | final OrgDecryptedContent content; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return Column( 15 | crossAxisAlignment: CrossAxisAlignment.stretch, 16 | children: [ 17 | if (content.content != null) OrgContentWidget(content.content!), 18 | ...content.sections.map((child) => OrgSectionWidget(child)), 19 | ], 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/widget/org_document.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/controller.dart'; 3 | import 'package:org_flutter/src/settings.dart'; 4 | import 'package:org_flutter/src/util/util.dart'; 5 | import 'package:org_flutter/src/widget/org_content.dart'; 6 | import 'package:org_flutter/src/widget/org_section.dart'; 7 | import 'package:org_flutter/src/widget/org_theme.dart'; 8 | import 'package:org_parser/org_parser.dart'; 9 | 10 | /// The root of the actual Org Mode document itself. Assumes that 11 | /// [OrgRootWidget] and [OrgController] are available in the build context. See 12 | /// the Org widget for a more user-friendly entrypoint. 13 | class OrgDocumentWidget extends StatelessWidget { 14 | const OrgDocumentWidget( 15 | this.document, { 16 | this.shrinkWrap = false, 17 | this.safeArea = true, 18 | super.key, 19 | }); 20 | 21 | final OrgDocument document; 22 | final bool shrinkWrap; 23 | final bool safeArea; 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return ListView( 28 | restorationId: shrinkWrap 29 | ? null 30 | : OrgController.of(context) 31 | .restorationIdFor('org_document_list_view'), 32 | padding: OrgTheme.dataOf(context).rootPadding, 33 | shrinkWrap: shrinkWrap, 34 | physics: shrinkWrap ? const NeverScrollableScrollPhysics() : null, 35 | children: [ 36 | if (document.content != null) ..._contentWidgets(context), 37 | ...document.sections.map((section) => OrgSectionWidget(section)), 38 | if (safeArea) listBottomSafeArea(), 39 | ], 40 | ); 41 | } 42 | 43 | Iterable _contentWidgets(BuildContext context) sync* { 44 | for (final child in document.content!.children) { 45 | Widget widget = OrgContentWidget(child); 46 | final textDirection = OrgSettings.of(context).settings.textDirection ?? 47 | child.detectTextDirection(); 48 | if (textDirection != null) { 49 | widget = Directionality(textDirection: textDirection, child: widget); 50 | } 51 | yield widget; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/src/widget/org_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/indent.dart'; 3 | import 'package:org_flutter/src/settings.dart'; 4 | import 'package:org_flutter/src/span.dart'; 5 | import 'package:org_flutter/src/util/util.dart'; 6 | import 'package:org_flutter/src/widget/org_content.dart'; 7 | import 'package:org_flutter/src/widget/org_theme.dart'; 8 | import 'package:org_parser/org_parser.dart'; 9 | 10 | /// An Org Mode drawer 11 | class OrgDrawerWidget extends StatefulWidget { 12 | const OrgDrawerWidget(this.drawer, {super.key}); 13 | final OrgDrawer drawer; 14 | 15 | @override 16 | State createState() => _OrgDrawerWidgetState(); 17 | } 18 | 19 | class _OrgDrawerWidgetState extends State 20 | with OpenCloseable { 21 | bool _inited = false; 22 | 23 | @override 24 | void didChangeDependencies() { 25 | super.didChangeDependencies(); 26 | if (!_inited) { 27 | openListenable.value = 28 | !OrgSettings.of(context).settings.hideDrawerStartup; 29 | _inited = true; 30 | } 31 | } 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | final body = IndentBuilder( 36 | widget.drawer.indent, 37 | builder: (context, totalIndentSize) { 38 | final defaultStyle = DefaultTextStyle.of(context).style; 39 | final drawerStyle = 40 | defaultStyle.copyWith(color: OrgTheme.dataOf(context).drawerColor); 41 | return ValueListenableBuilder( 42 | valueListenable: openListenable, 43 | builder: (context, open, child) { 44 | final trailingWidget = _trailing(); 45 | return Column( 46 | crossAxisAlignment: CrossAxisAlignment.stretch, 47 | children: [ 48 | InkWell( 49 | onTap: () => openListenable.value = !openListenable.value, 50 | child: Text( 51 | widget.drawer.header.trimRight() + (open ? '' : '...'), 52 | style: drawerStyle, 53 | ), 54 | ), 55 | AnimatedSwitcher( 56 | duration: const Duration(milliseconds: 100), 57 | transitionBuilder: (child, animation) => 58 | SizeTransition(sizeFactor: animation, child: child), 59 | child: open ? child : const SizedBox.shrink(), 60 | ), 61 | if (trailingWidget != null) trailingWidget, 62 | ], 63 | ); 64 | }, 65 | child: Column( 66 | crossAxisAlignment: CrossAxisAlignment.stretch, 67 | children: [ 68 | _body((_, string) => removeTrailingLineBreak( 69 | hardDeindent(string, totalIndentSize))), 70 | Text( 71 | hardDeindent(widget.drawer.footer, totalIndentSize), 72 | style: drawerStyle, 73 | ), 74 | ], 75 | ), 76 | ); 77 | }, 78 | ); 79 | return reduceOpacity( 80 | body, 81 | enabled: OrgSettings.of(context).settings.deemphasizeMarkup, 82 | ); 83 | } 84 | 85 | Widget _body(Transformer transformer) { 86 | final body = OrgContentWidget( 87 | widget.drawer.body, 88 | transformer: transformer, 89 | ); 90 | // TODO(aaron): Better distinguish "greater block" from regular block 91 | return widget.drawer.body is OrgContent 92 | ? body 93 | : SingleChildScrollView( 94 | scrollDirection: Axis.horizontal, 95 | physics: const AlwaysScrollableScrollPhysics(), 96 | child: body, 97 | ); 98 | } 99 | 100 | Widget? _trailing() { 101 | var trailing = removeTrailingLineBreak(widget.drawer.trailing); 102 | // If trailing is empty here then there is something immediately following 103 | // the drawer. Because we render the drawer with full width, any trailing 104 | // Text widget will result in an unwanted empty line. Thus we return null. 105 | if (trailing.isEmpty) return null; 106 | // We have to remove another linebreak because there will be an implicit 107 | // linebreak when this widget ends. 108 | return Text(removeTrailingLineBreak(trailing)); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/widget/org_dynamic_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/indent.dart'; 3 | import 'package:org_flutter/src/settings.dart'; 4 | import 'package:org_flutter/src/util/util.dart'; 5 | import 'package:org_flutter/src/widget/org_content.dart'; 6 | import 'package:org_flutter/src/widget/org_theme.dart'; 7 | import 'package:org_parser/org_parser.dart'; 8 | 9 | /// An Org Mode dynamic block 10 | class OrgDynamicBlockWidget extends StatefulWidget { 11 | const OrgDynamicBlockWidget(this.block, {super.key}); 12 | final OrgDynamicBlock block; 13 | 14 | @override 15 | State createState() => _OrgDynamicBlockWidgetState(); 16 | } 17 | 18 | class _OrgDynamicBlockWidgetState extends State 19 | with OpenCloseable { 20 | bool _inited = false; 21 | 22 | @override 23 | void didChangeDependencies() { 24 | super.didChangeDependencies(); 25 | if (!_inited) { 26 | openListenable.value = !OrgSettings.of(context).settings.hideBlockStartup; 27 | _inited = true; 28 | } 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | final defaultStyle = DefaultTextStyle.of(context).style; 34 | final metaStyle = 35 | defaultStyle.copyWith(color: OrgTheme.dataOf(context).metaColor); 36 | final hideMarkup = OrgSettings.of(context).settings.deemphasizeMarkup; 37 | // Remove a line break because we introduce one by splitting the text into 38 | // two widgets in this Column 39 | final trailing = removeTrailingLineBreak(widget.block.trailing); 40 | return IndentBuilder( 41 | widget.block.indent, 42 | builder: (context, totalIndentSize) { 43 | return ValueListenableBuilder( 44 | valueListenable: openListenable, 45 | builder: (context, open, child) => Column( 46 | crossAxisAlignment: CrossAxisAlignment.stretch, 47 | children: [ 48 | _header(context, metaStyle, open: open, hideMarkup: hideMarkup), 49 | AnimatedSwitcher( 50 | duration: const Duration(milliseconds: 100), 51 | transitionBuilder: (child, animation) => 52 | SizeTransition(sizeFactor: animation, child: child), 53 | child: open ? child : const SizedBox.shrink(), 54 | ), 55 | if (trailing.isNotEmpty) 56 | // Remove another line break because the existence of even an 57 | // empty string here takes up a line. 58 | Text(removeTrailingLineBreak(trailing)), 59 | ], 60 | ), 61 | child: Column( 62 | crossAxisAlignment: CrossAxisAlignment.stretch, 63 | children: [ 64 | OrgContentWidget(widget.block.body), 65 | reduceOpacity( 66 | Text( 67 | hardDeindent(widget.block.footer, totalIndentSize), 68 | style: metaStyle, 69 | ), 70 | enabled: hideMarkup, 71 | ), 72 | ], 73 | ), 74 | ); 75 | }, 76 | ); 77 | } 78 | 79 | Widget _header( 80 | BuildContext context, 81 | TextStyle metaStyle, { 82 | required bool hideMarkup, 83 | required bool open, 84 | }) { 85 | var text = widget.block.header.trimRight(); 86 | if (!hideMarkup && !open) { 87 | text += '...'; 88 | } 89 | Widget header = Text( 90 | text, 91 | style: metaStyle, 92 | softWrap: !hideMarkup, 93 | overflow: hideMarkup ? TextOverflow.fade : null, 94 | ); 95 | if (hideMarkup && !open) { 96 | header = Row( 97 | children: [ 98 | Flexible( 99 | child: header, 100 | ), 101 | if (hideMarkup && !open) Text('...', style: metaStyle) 102 | ], 103 | ); 104 | } 105 | header = reduceOpacity(header, enabled: hideMarkup); 106 | return InkWell( 107 | onTap: () => openListenable.value = !openListenable.value, 108 | child: header, 109 | ); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/widget/org_fixed_width_area.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/indent.dart'; 3 | import 'package:org_flutter/src/span.dart'; 4 | import 'package:org_flutter/src/util/util.dart'; 5 | import 'package:org_flutter/src/widget/org_theme.dart'; 6 | import 'package:org_parser/org_parser.dart'; 7 | 8 | /// An Org Mode fixed-width area 9 | class OrgFixedWidthAreaWidget extends StatelessWidget { 10 | const OrgFixedWidthAreaWidget(this.fixedWidthArea, {super.key}); 11 | final OrgFixedWidthArea fixedWidthArea; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return IndentBuilder( 16 | fixedWidthArea.indent, 17 | builder: (context, totalIndentSize) { 18 | return DefaultTextStyle.merge( 19 | style: TextStyle(color: OrgTheme.dataOf(context).codeColor), 20 | child: SingleChildScrollView( 21 | scrollDirection: Axis.horizontal, 22 | physics: const AlwaysScrollableScrollPhysics(), 23 | child: FancySpanBuilder( 24 | builder: (context, spanBuilder) => Text.rich( 25 | spanBuilder.highlightedSpan( 26 | removeTrailingLineBreak(hardDeindent( 27 | fixedWidthArea.content + fixedWidthArea.trailing, 28 | totalIndentSize)), 29 | ), 30 | ), 31 | ), 32 | ), 33 | ); 34 | }, 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/widget/org_footnote_reference.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/flash.dart'; 3 | import 'package:org_flutter/src/locator.dart'; 4 | import 'package:org_flutter/src/span.dart'; 5 | import 'package:org_flutter/src/widget/org_theme.dart'; 6 | import 'package:org_parser/org_parser.dart'; 7 | 8 | typedef FootnoteKey = GlobalKey; 9 | 10 | class OrgFootnoteReferenceWidget extends StatefulWidget { 11 | const OrgFootnoteReferenceWidget(this.reference, {super.key}); 12 | final OrgFootnoteReference reference; 13 | 14 | @override 15 | State createState() => 16 | OrgFootnoteReferenceWidgetState(); 17 | } 18 | 19 | class OrgFootnoteReferenceWidgetState 20 | extends State { 21 | bool _cookie = true; 22 | 23 | void doHighlight() => setState(() => _cookie = !_cookie); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final defaultStyle = DefaultTextStyle.of(context).style; 28 | final footnoteStyle = defaultStyle.copyWith( 29 | color: OrgTheme.dataOf(context).footnoteColor, 30 | ); 31 | 32 | return FancySpanBuilder( 33 | builder: (context, spanBuilder) => InkWell( 34 | onTap: widget.reference.name == null 35 | ? null 36 | : () => OrgLocator.of(context)?.jumpToFootnote(widget.reference), 37 | child: AnimatedTextFlash( 38 | cookie: _cookie, 39 | child: Text.rich( 40 | TextSpan( 41 | children: [ 42 | spanBuilder.highlightedSpan(widget.reference.leading, 43 | style: footnoteStyle), 44 | if (widget.reference.name != null) 45 | spanBuilder.highlightedSpan(widget.reference.name!, 46 | style: footnoteStyle), 47 | if (widget.reference.definition != null) 48 | spanBuilder.highlightedSpan( 49 | widget.reference.definition!.delimiter, 50 | style: footnoteStyle), 51 | if (widget.reference.definition != null) 52 | spanBuilder.build( 53 | widget.reference.definition!.value, 54 | style: footnoteStyle, 55 | ), 56 | spanBuilder.highlightedSpan(widget.reference.trailing, 57 | style: footnoteStyle), 58 | ], 59 | ), 60 | ), 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/widget/org_horizontal_rule.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/util/util.dart'; 3 | import 'package:org_parser/org_parser.dart'; 4 | 5 | class OrgHorizontalRuleWidget extends StatelessWidget { 6 | const OrgHorizontalRuleWidget(this.hr, {super.key}); 7 | final OrgHorizontalRule hr; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final defaultStyle = DefaultTextStyle.of(context).style; 12 | final trailing = removeTrailingLineBreak(hr.trailing); 13 | return Text.rich( 14 | TextSpan( 15 | children: [ 16 | WidgetSpan( 17 | child: Container( 18 | color: defaultStyle.color ?? Colors.black, 19 | width: double.infinity, 20 | height: 1, 21 | ), 22 | alignment: PlaceholderAlignment.middle, 23 | ), 24 | if (trailing.isNotEmpty) TextSpan(text: trailing), 25 | ], 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/widget/org_inline_src_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/highlight.dart'; 3 | import 'package:org_flutter/src/util/util.dart'; 4 | import 'package:org_flutter/src/widget/org_theme.dart'; 5 | import 'package:org_parser/org_parser.dart'; 6 | 7 | /// An Org Mode inline source block 8 | class OrgInlineSrcBlockWidget extends StatelessWidget { 9 | const OrgInlineSrcBlockWidget(this.block, {super.key}); 10 | 11 | final OrgInlineSrcBlock block; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final defaultStyle = DefaultTextStyle.of(context).style; 16 | final orgTheme = OrgTheme.dataOf(context); 17 | final codeStyle = defaultStyle.copyWith(color: orgTheme.codeColor); 18 | final metaStyle = defaultStyle.copyWith(color: orgTheme.metaColor); 19 | return Text.rich(TextSpan(children: [ 20 | TextSpan(text: block.leading, style: codeStyle), 21 | TextSpan(text: block.language, style: metaStyle), 22 | if (block.arguments != null) 23 | TextSpan(text: block.arguments, style: codeStyle), 24 | TextSpan(text: '{', style: codeStyle), 25 | if (supportedSrcLanguage(block.language)) 26 | buildSrcHighlightSpan( 27 | context, 28 | code: trimPrefSuff(block.body, '{', '}'), 29 | languageId: block.language, 30 | ) 31 | else 32 | TextSpan(text: trimPrefSuff(block.body, '{', '}'), style: codeStyle), 33 | TextSpan(text: '}', style: codeStyle), 34 | ])); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/widget/org_latex_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_tex_js/flutter_tex_js.dart'; 3 | import 'package:org_flutter/src/util/util.dart'; 4 | import 'package:org_parser/org_parser.dart'; 5 | 6 | /// An Org Mode LaTeX block 7 | class OrgLatexBlockWidget extends StatelessWidget { 8 | const OrgLatexBlockWidget(this.block, {super.key}); 9 | 10 | final OrgLatexBlock block; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | // Remove a line break because we introduce one by splitting the text into 15 | // two widgets in this Column 16 | final trailing = removeTrailingLineBreak(block.trailing); 17 | return Column( 18 | children: [ 19 | ConstrainedBox( 20 | constraints: const BoxConstraints.tightFor(width: double.infinity), 21 | child: SingleChildScrollView( 22 | scrollDirection: Axis.horizontal, 23 | child: TexImage( 24 | _content, 25 | displayMode: true, 26 | error: (context, error) { 27 | debugPrint(error.toString()); 28 | return Text(block.toMarkup()); 29 | }, 30 | ), 31 | ), 32 | ), 33 | if (trailing.isNotEmpty) 34 | // Remove another line break because the existence of even an empty 35 | // string here takes up a line. 36 | Text(removeTrailingLineBreak(trailing)), 37 | ], 38 | ); 39 | } 40 | 41 | String get _content { 42 | if (flutterTexJsSupportedEnvironments.contains(block.environment)) { 43 | return '${block.begin}${block.content}${block.end}'; 44 | } else { 45 | return block.content; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/widget/org_latex_inline.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_tex_js/flutter_tex_js.dart'; 3 | import 'package:org_parser/org_parser.dart'; 4 | 5 | /// An Org Mode LaTeX inline span 6 | class OrgLatexInlineWidget extends StatelessWidget { 7 | const OrgLatexInlineWidget(this.latex, {super.key}); 8 | 9 | final OrgLatexInline latex; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return TexImage( 14 | latex.content, 15 | displayMode: false, 16 | error: (context, error) { 17 | debugPrint(error.toString()); 18 | return Text([ 19 | latex.leadingDecoration, 20 | latex.content, 21 | latex.trailingDecoration, 22 | ].join('')); 23 | }, 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/widget/org_link.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/events.dart'; 3 | import 'package:org_flutter/src/span.dart'; 4 | import 'package:org_parser/org_parser.dart'; 5 | 6 | /// A widget to display an [OrgLink]. 7 | /// 8 | /// This is not produced in the normal flow of things; rather [OrgSpanBuilder] 9 | /// produces an inline [TextSpan] for OrgLinks. However consumers of org_flutter 10 | /// may want this when e.g. an image widget supplied to [OrgEvents.loadImage] 11 | /// fails to load the image, and as a fallback the consumer wants to display the 12 | /// link as it would have been shown had it been treated as a text link. 13 | /// 14 | /// This widget will *not* attempt to render a link as an image. 15 | class OrgLinkWidget extends StatelessWidget { 16 | const OrgLinkWidget(this.link, {super.key}); 17 | 18 | final OrgLink link; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return FancySpanBuilder( 23 | inlineImages: false, 24 | builder: (context, spanBuilder) => InkWell( 25 | onTap: () => OrgEvents.of(context).onLinkTap?.call(link), 26 | child: Text.rich(spanBuilder.build(link)), 27 | ), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/widget/org_link_target.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | import 'package:org_flutter/src/flash.dart'; 4 | import 'package:org_flutter/src/span.dart'; 5 | 6 | typedef LinkTargetKey = GlobalKey; 7 | 8 | class OrgLinkTargetWidget extends StatefulWidget { 9 | const OrgLinkTargetWidget(this.radioTarget, {super.key}); 10 | 11 | final OrgLinkTarget radioTarget; 12 | 13 | @override 14 | State createState() => OrgLinkTargetWidgetState(); 15 | } 16 | 17 | class OrgLinkTargetWidgetState extends State { 18 | bool _cookie = true; 19 | 20 | void doHighlight() => setState(() => _cookie = !_cookie); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final defaultStyle = DefaultTextStyle.of(context).style; 25 | final targetStyle = defaultStyle.copyWith( 26 | decoration: TextDecoration.underline, 27 | ); 28 | 29 | return FancySpanBuilder( 30 | builder: (context, spanBuilder) => AnimatedTextFlash( 31 | cookie: _cookie, 32 | child: Text.rich( 33 | TextSpan( 34 | children: [ 35 | spanBuilder.highlightedSpan(widget.radioTarget.leading, 36 | style: targetStyle), 37 | spanBuilder.highlightedSpan(widget.radioTarget.body, 38 | style: targetStyle), 39 | spanBuilder.highlightedSpan(widget.radioTarget.trailing, 40 | style: targetStyle), 41 | ], 42 | ), 43 | ), 44 | ), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/widget/org_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/events.dart'; 3 | import 'package:org_flutter/src/indent.dart'; 4 | import 'package:org_flutter/src/settings.dart'; 5 | import 'package:org_flutter/src/span.dart'; 6 | import 'package:org_flutter/src/util/util.dart'; 7 | import 'package:org_parser/org_parser.dart'; 8 | 9 | /// An Org Mode list 10 | class OrgListWidget extends StatelessWidget { 11 | const OrgListWidget(this.list, {super.key}); 12 | final OrgList list; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Column( 17 | crossAxisAlignment: CrossAxisAlignment.stretch, 18 | children: _children.toList(growable: false), 19 | ); 20 | } 21 | 22 | Iterable get _children sync* { 23 | for (final item in list.items) { 24 | yield _OrgListItemWidget(item); 25 | } 26 | if (list.trailing.isNotEmpty) { 27 | // Remove a line break because we introduce one by splitting the text into 28 | // two widgets in this Column 29 | yield Text(removeTrailingLineBreak(list.trailing)); 30 | } 31 | } 32 | } 33 | 34 | /// An Org Mode list item 35 | class _OrgListItemWidget extends StatelessWidget { 36 | const _OrgListItemWidget(this.item); 37 | final OrgListItem item; 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | return IndentBuilder( 42 | '${item.indent}${item.bullet}', 43 | builder: (context, totalIndentSize) => InkWell( 44 | onTap: _hasCheckbox 45 | ? () => OrgEvents.of(context).onListItemTap?.call(item) 46 | : null, 47 | child: FancySpanBuilder( 48 | builder: (context, spanBuilder) => Text.rich( 49 | TextSpan( 50 | children: _spans(context, spanBuilder, totalIndentSize) 51 | .toList(growable: false), 52 | ), 53 | ), 54 | ), 55 | ), 56 | ); 57 | } 58 | 59 | bool get _hasCheckbox => item.checkbox != null; 60 | 61 | Iterable _spans( 62 | BuildContext context, 63 | OrgSpanBuilder builder, 64 | int totalIndentSize, 65 | ) sync* { 66 | final item = this.item; 67 | if (item is OrgListOrderedItem && item.counterSet != null) { 68 | yield builder.highlightedSpan( 69 | '${item.counterSet} ', 70 | style: DefaultTextStyle.of(context) 71 | .style 72 | .copyWith(fontWeight: FontWeight.bold), 73 | ); 74 | } 75 | if (item.checkbox != null) { 76 | yield builder.highlightedSpan( 77 | '${item.checkbox} ', 78 | style: DefaultTextStyle.of(context) 79 | .style 80 | .copyWith(fontWeight: FontWeight.bold), 81 | ); 82 | } 83 | if (item is OrgListUnorderedItem && item.tag != null) { 84 | final style = DefaultTextStyle.of(context) 85 | .style 86 | .copyWith(fontWeight: FontWeight.bold); 87 | yield TextSpan(children: [ 88 | builder.build(item.tag!.value, style: style), 89 | builder.highlightedSpan(item.tag!.delimiter, style: style), 90 | ]); 91 | } 92 | if (item.body != null) { 93 | final reflow = OrgSettings.of(context).settings.reflowText; 94 | yield builder.build(item.body!, transformer: (elem, content) { 95 | final location = locationOf(elem, item.body!.children); 96 | var formattedContent = hardDeindent(content, totalIndentSize); 97 | if (reflow) { 98 | formattedContent = reflowText(formattedContent, location); 99 | } 100 | if (location == TokenLocation.end || location == TokenLocation.only) { 101 | final last = removeTrailingLineBreak(formattedContent); 102 | // A trailing linebreak results in a line with the same height as 103 | // the previous line. This is bad when the previous line is 104 | // artificially tall due to a WidgetSpan (especially an image). To 105 | // avoid this we add a zero-width space to the end if the text has 106 | // a single, trailing linebreak. 107 | // 108 | // See: https://github.com/flutter/flutter/issues/156268 109 | // 110 | // TODO(aaron): Limit to when the previous element is a link? 111 | return last.indexOf('\n') == last.length - 1 ? '$last\u200b' : last; 112 | } else { 113 | return formattedContent; 114 | } 115 | }); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/src/widget/org_local_variables.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/span.dart'; 3 | import 'package:org_flutter/src/util/util.dart'; 4 | import 'package:org_flutter/src/widget/org_theme.dart'; 5 | import 'package:org_parser/org_parser.dart'; 6 | 7 | /// An Org Local Variables block 8 | class OrgLocalVariablesWidget extends StatelessWidget { 9 | const OrgLocalVariablesWidget(this.variables, {super.key}); 10 | final OrgLocalVariables variables; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | final defaultStyle = DefaultTextStyle.of(context).style; 15 | final metaStyle = 16 | defaultStyle.copyWith(color: OrgTheme.dataOf(context).metaColor); 17 | 18 | return SingleChildScrollView( 19 | scrollDirection: Axis.horizontal, 20 | child: FancySpanBuilder( 21 | builder: (context, spanBuilder) => Text.rich( 22 | TextSpan(children: [ 23 | spanBuilder.highlightedSpan(variables.start, style: metaStyle), 24 | for (final entry in variables.entries) 25 | spanBuilder.highlightedSpan( 26 | entry.prefix + entry.content + entry.suffix, 27 | style: metaStyle, 28 | ), 29 | spanBuilder.highlightedSpan(variables.end, style: metaStyle), 30 | spanBuilder.highlightedSpan( 31 | removeTrailingLineBreak(variables.trailing), 32 | style: metaStyle), 33 | ]), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/widget/org_meta.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/flash.dart'; 3 | import 'package:org_flutter/src/indent.dart'; 4 | import 'package:org_flutter/src/settings.dart'; 5 | import 'package:org_flutter/src/span.dart'; 6 | import 'package:org_flutter/src/util/util.dart'; 7 | import 'package:org_flutter/src/widget/org_theme.dart'; 8 | import 'package:org_parser/org_parser.dart'; 9 | 10 | typedef NameKey = GlobalKey; 11 | 12 | const _kDocInfoKeywords = { 13 | '#+TITLE:', 14 | '#+SUBTITLE:', 15 | '#+AUTHOR:', 16 | '#+EMAIL:', 17 | '#+DATE:' 18 | }; 19 | 20 | const _kExportedKeywords = { 21 | ..._kDocInfoKeywords, 22 | '#+CAPTION:', 23 | }; 24 | 25 | /// An Org Mode meta line 26 | class OrgMetaWidget extends StatefulWidget { 27 | const OrgMetaWidget(this.meta, {super.key}); 28 | final OrgMeta meta; 29 | 30 | @override 31 | State createState() => OrgMetaWidgetState(); 32 | } 33 | 34 | class OrgMetaWidgetState extends State { 35 | bool _cookie = true; 36 | 37 | void doHighlight() => setState(() => _cookie = !_cookie); 38 | 39 | @override 40 | Widget build(BuildContext context) { 41 | final deemphasize = !_isDocInfoKeyword && 42 | OrgSettings.of(context).settings.deemphasizeMarkup; 43 | return IndentBuilder( 44 | widget.meta.indent, 45 | builder: (context, _) { 46 | Widget body = FancySpanBuilder( 47 | builder: (context, spanBuilder) => AnimatedTextFlash( 48 | cookie: _cookie, 49 | child: Text.rich( 50 | TextSpan( 51 | children: _spans(context, spanBuilder).toList(growable: false), 52 | ), 53 | softWrap: !deemphasize, 54 | ), 55 | ), 56 | ); 57 | if (!_isExportedKeyword) { 58 | body = InheritedOrgSettings.merge( 59 | OrgSettings(strictSubSuperscripts: true), 60 | child: body, 61 | ); 62 | } 63 | if (deemphasize) { 64 | body = reduceOpacity(SingleChildScrollView( 65 | scrollDirection: Axis.horizontal, 66 | child: body, 67 | )); 68 | } 69 | return body; 70 | }, 71 | ); 72 | } 73 | 74 | bool get _isDocInfoKeyword => 75 | _kDocInfoKeywords.contains(widget.meta.key.toUpperCase()); 76 | 77 | bool get _isExportedKeyword => 78 | _kExportedKeywords.contains(widget.meta.key.toUpperCase()); 79 | 80 | TextStyle? _keywordStyle(BuildContext context) { 81 | final style = DefaultTextStyle.of(context).style; 82 | return _isDocInfoKeyword 83 | ? style.copyWith(color: OrgTheme.dataOf(context).codeColor) 84 | : style.copyWith(color: OrgTheme.dataOf(context).metaColor); 85 | } 86 | 87 | TextStyle? _valueStyle(BuildContext context) { 88 | final style = DefaultTextStyle.of(context).style; 89 | return _isDocInfoKeyword 90 | ? style.copyWith( 91 | color: OrgTheme.dataOf(context).infoColor, 92 | fontWeight: widget.meta.key.toUpperCase() == '#+TITLE:' 93 | ? FontWeight.bold 94 | : null, 95 | ) 96 | : style.copyWith(color: OrgTheme.dataOf(context).metaColor); 97 | } 98 | 99 | Iterable _spans( 100 | BuildContext context, OrgSpanBuilder builder) sync* { 101 | yield builder.highlightedSpan(widget.meta.key, 102 | style: _keywordStyle(context)); 103 | if (widget.meta.value != null) { 104 | yield builder.build(widget.meta.value!, style: _valueStyle(context)); 105 | } 106 | final trailing = removeTrailingLineBreak(widget.meta.trailing); 107 | if (trailing.isNotEmpty) { 108 | yield builder.highlightedSpan(trailing, style: _valueStyle(context)); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /lib/src/widget/org_paragraph.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/indent.dart'; 3 | import 'package:org_flutter/src/settings.dart'; 4 | import 'package:org_flutter/src/span.dart'; 5 | import 'package:org_flutter/src/util/util.dart'; 6 | import 'package:org_parser/org_parser.dart'; 7 | 8 | /// An Org Mode paragraph 9 | class OrgParagraphWidget extends StatelessWidget { 10 | const OrgParagraphWidget(this.paragraph, {super.key}); 11 | final OrgParagraph paragraph; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final reflow = OrgSettings.of(context).settings.reflowText; 16 | return IndentBuilder( 17 | paragraph.indent, 18 | builder: (context, totalIndentSize) { 19 | return FancySpanBuilder( 20 | builder: (context, spanBuilder) => Text.rich(TextSpan(children: [ 21 | spanBuilder.build( 22 | paragraph.body, 23 | transformer: (elem, content) { 24 | final location = locationOf(elem, paragraph.body.children); 25 | var formattedContent = hardDeindent(content, totalIndentSize); 26 | if (reflow) { 27 | formattedContent = reflowText(formattedContent, location); 28 | } 29 | if (location == TokenLocation.end || 30 | location == TokenLocation.only && 31 | paragraph.trailing.isEmpty) { 32 | formattedContent = removeTrailingLineBreak(formattedContent); 33 | } 34 | return formattedContent; 35 | }, 36 | ), 37 | if (paragraph.trailing.isNotEmpty) _trailingSpan(), 38 | ])), 39 | ); 40 | }, 41 | ); 42 | } 43 | 44 | TextSpan _trailingSpan() { 45 | var trailing = removeTrailingLineBreak(paragraph.trailing); 46 | // A trailing linebreak results in a line with the same height as 47 | // the previous line. This is bad when the previous line is 48 | // artificially tall due to a WidgetSpan (especially an image). To 49 | // avoid this we add a zero-width space to the end if the text has 50 | // a single, trailing linebreak. 51 | // 52 | // See: https://github.com/flutter/flutter/issues/156268 53 | // 54 | // TODO(aaron): Limit to when the previous element is a link? 55 | if (trailing.indexOf('\n') == trailing.length - 1) { 56 | trailing = '$trailing\u200b'; 57 | } 58 | return TextSpan(text: trailing); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/widget/org_pgp_block.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/span.dart'; 3 | import 'package:org_flutter/src/util/util.dart'; 4 | import 'package:org_parser/org_parser.dart'; 5 | 6 | /// An Org PGP block 7 | class OrgPgpBlockWidget extends StatelessWidget { 8 | const OrgPgpBlockWidget(this.block, {super.key}); 9 | final OrgPgpBlock block; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return SingleChildScrollView( 14 | scrollDirection: Axis.horizontal, 15 | child: FancySpanBuilder( 16 | builder: (context, spanBuilder) => Text.rich( 17 | TextSpan(children: [ 18 | spanBuilder.highlightedSpan(block.indent), 19 | spanBuilder.highlightedSpan(block.header), 20 | spanBuilder.highlightedSpan(block.body), 21 | spanBuilder.highlightedSpan(block.footer), 22 | spanBuilder 23 | .highlightedSpan(removeTrailingLineBreak(block.trailing)), 24 | ]), 25 | ), 26 | ), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/widget/org_property.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/indent.dart'; 3 | import 'package:org_flutter/src/span.dart'; 4 | import 'package:org_flutter/src/util/util.dart'; 5 | import 'package:org_flutter/src/widget/org_theme.dart'; 6 | import 'package:org_parser/org_parser.dart'; 7 | 8 | /// An Org Mode property 9 | class OrgPropertyWidget extends StatelessWidget { 10 | const OrgPropertyWidget(this.property, {super.key}); 11 | final OrgProperty property; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return IndentBuilder( 16 | property.indent, 17 | builder: (context, _) { 18 | return FancySpanBuilder( 19 | builder: (context, spanBuilder) => SingleChildScrollView( 20 | scrollDirection: Axis.horizontal, 21 | physics: const AlwaysScrollableScrollPhysics(), 22 | child: Text.rich( 23 | TextSpan( 24 | children: _spans(context, spanBuilder).toList(growable: false), 25 | ), 26 | ), 27 | ), 28 | ); 29 | }, 30 | ); 31 | } 32 | 33 | Iterable _spans( 34 | BuildContext context, OrgSpanBuilder builder) sync* { 35 | yield builder.highlightedSpan( 36 | property.key, 37 | style: DefaultTextStyle.of(context) 38 | .style 39 | .copyWith(color: OrgTheme.dataOf(context).keywordColor), 40 | ); 41 | yield builder.build(property.value); 42 | final trailing = removeTrailingLineBreak(property.trailing); 43 | if (trailing.isNotEmpty) { 44 | yield builder.highlightedSpan(trailing); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/widget/org_radio_target.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | import 'package:org_flutter/src/flash.dart'; 4 | import 'package:org_flutter/src/span.dart'; 5 | 6 | typedef RadioTargetKey = GlobalKey; 7 | 8 | class OrgRadioTargetWidget extends StatefulWidget { 9 | const OrgRadioTargetWidget(this.radioTarget, {super.key}); 10 | 11 | final OrgRadioTarget radioTarget; 12 | 13 | @override 14 | State createState() => OrgRadioTargetWidgetState(); 15 | } 16 | 17 | class OrgRadioTargetWidgetState extends State { 18 | bool _cookie = true; 19 | 20 | void doHighlight() => setState(() => _cookie = !_cookie); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final defaultStyle = DefaultTextStyle.of(context).style; 25 | final targetStyle = defaultStyle.copyWith( 26 | decoration: TextDecoration.underline, 27 | ); 28 | 29 | return FancySpanBuilder( 30 | builder: (context, spanBuilder) => AnimatedTextFlash( 31 | cookie: _cookie, 32 | child: Text.rich( 33 | TextSpan( 34 | children: [ 35 | spanBuilder.highlightedSpan( 36 | widget.radioTarget.leading, 37 | style: targetStyle, 38 | ), 39 | spanBuilder.highlightedSpan( 40 | widget.radioTarget.body, 41 | style: targetStyle.copyWith( 42 | color: OrgTheme.dataOf(context).linkColor, 43 | ), 44 | ), 45 | spanBuilder.highlightedSpan( 46 | widget.radioTarget.trailing, 47 | style: targetStyle, 48 | ), 49 | ], 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/widget/org_root.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/events.dart'; 3 | import 'package:org_flutter/src/settings.dart'; 4 | import 'package:org_flutter/src/theme.dart'; 5 | import 'package:org_flutter/src/util/util.dart'; 6 | import 'package:org_flutter/src/widget/org_theme.dart'; 7 | import 'package:org_parser/org_parser.dart'; 8 | 9 | /// A widget that sits above the [OrgDocumentWidget] and orchestrates [OrgTheme] 10 | /// and [OrgEvents]. 11 | class OrgRootWidget extends StatelessWidget { 12 | const OrgRootWidget({ 13 | required this.child, 14 | this.style, 15 | this.lightTheme, 16 | this.darkTheme, 17 | this.onLinkTap, 18 | this.onLocalSectionLinkTap, 19 | this.onSectionLongPress, 20 | this.onSectionSlide, 21 | this.onListItemTap, 22 | this.onCitationTap, 23 | this.onTimestampTap, 24 | this.loadImage, 25 | super.key, 26 | }); 27 | 28 | final Widget child; 29 | 30 | /// Text style to serve as a basis for all text in the document 31 | final TextStyle? style; 32 | 33 | final OrgThemeData? lightTheme; 34 | final OrgThemeData? darkTheme; 35 | 36 | /// A callback invoked when the user taps a link. The argument is the 37 | /// [OrgLink] object; the URL is [OrgLink.location]. You might want to open 38 | /// this in a browser. 39 | final void Function(OrgLink)? onLinkTap; 40 | 41 | /// A callback invoked when the user taps on a link to a section within the 42 | /// current document. The argument is the target section. You might want to 43 | /// display it somehow. 44 | final void Function(OrgTree)? onLocalSectionLinkTap; 45 | 46 | /// A callback invoked when the user long-presses on a section headline within 47 | /// the current document. The argument is the pressed section. You might want 48 | /// to narrow the display to show just this section. 49 | final void Function(OrgSection)? onSectionLongPress; 50 | 51 | /// A callback invoked to build a list of actions revealed when the user 52 | /// slides a section. The argument is the section being slid. Consider 53 | /// supplying instances of `SlidableAction` from the 54 | /// [flutter_slidable](https://pub.dev/packages/flutter_slidable) package. 55 | final List Function(OrgSection)? onSectionSlide; 56 | 57 | /// A callback invoked when the user taps on a list item that has a checkbox 58 | /// within the current document. The argument is the tapped item. You might 59 | /// want to toggle the checkbox. 60 | final void Function(OrgListItem)? onListItemTap; 61 | 62 | /// A callback invoked when the user taps on a citation. 63 | final void Function(OrgCitation)? onCitationTap; 64 | 65 | /// A callback invoked when the user taps on a timestamp. 66 | final void Function(OrgNode)? onTimestampTap; 67 | 68 | /// A callback invoked when an image should be displayed. The argument is the 69 | /// [OrgLink] describing where the image data can be found. It is your 70 | /// responsibility to resolve the link, fetch the data, and return a widget 71 | /// for displaying the image. 72 | /// 73 | /// Return null instead to display the link text. 74 | final Widget? Function(OrgLink)? loadImage; 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | Widget body = OrgTheme( 79 | light: lightTheme ?? OrgThemeData.light(), 80 | dark: darkTheme ?? OrgThemeData.dark(), 81 | child: OrgEvents( 82 | onLinkTap: onLinkTap, 83 | onSectionLongPress: onSectionLongPress, 84 | onSectionSlide: onSectionSlide, 85 | onLocalSectionLinkTap: onLocalSectionLinkTap, 86 | loadImage: loadImage, 87 | onListItemTap: onListItemTap, 88 | onCitationTap: onCitationTap, 89 | onTimestampTap: onTimestampTap, 90 | child: IdentityTextScale(child: child), 91 | ), 92 | ); 93 | final locale = OrgSettings.of(context).settings.locale; 94 | if (locale != null) { 95 | body = Localizations.override( 96 | context: context, 97 | locale: locale, 98 | child: body, 99 | ); 100 | } 101 | return style == null 102 | ? body 103 | : DefaultTextStyle.merge( 104 | style: style, 105 | child: body, 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/src/widget/org_section.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_slidable/flutter_slidable.dart'; 3 | import 'package:org_flutter/src/controller.dart'; 4 | import 'package:org_flutter/src/events.dart'; 5 | import 'package:org_flutter/src/settings.dart'; 6 | import 'package:org_flutter/src/util/util.dart'; 7 | import 'package:org_flutter/src/widget/org_content.dart'; 8 | import 'package:org_flutter/src/widget/org_headline.dart'; 9 | import 'package:org_flutter/src/widget/org_theme.dart'; 10 | import 'package:org_parser/org_parser.dart'; 11 | 12 | /// An Org Mode section 13 | class OrgSectionWidget extends StatelessWidget { 14 | const OrgSectionWidget( 15 | this.section, { 16 | this.root = false, 17 | this.shrinkWrap = false, 18 | super.key, 19 | }); 20 | final OrgSection section; 21 | final bool root; 22 | final bool shrinkWrap; 23 | 24 | // Whether the section is open "enough" to not show the trailing ellipsis 25 | bool _openEnough(OrgVisibilityState visibility) { 26 | switch (visibility) { 27 | case OrgVisibilityState.folded: 28 | return section.isEmpty; 29 | case OrgVisibilityState.contents: 30 | return section.content == null; 31 | case OrgVisibilityState.children: 32 | case OrgVisibilityState.subtree: 33 | return true; 34 | case OrgVisibilityState.hidden: 35 | // Not meaningful 36 | return false; 37 | } 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | final visibilityListenable = 43 | OrgController.of(context).nodeFor(section).visibility; 44 | final widget = ValueListenableBuilder( 45 | valueListenable: visibilityListenable, 46 | builder: (context, visibility, child) => visibility == 47 | OrgVisibilityState.hidden 48 | ? const SizedBox.shrink() 49 | : ListView( 50 | shrinkWrap: shrinkWrap || !root, 51 | physics: shrinkWrap || !root 52 | ? const NeverScrollableScrollPhysics() 53 | : null, 54 | // It's very important that the padding not be null here; otherwise 55 | // sections inside a root document will get some extraneous padding (see 56 | // discussion of padding behavior on ListView) 57 | padding: 58 | root ? OrgTheme.dataOf(context).rootPadding : EdgeInsets.zero, 59 | children: [ 60 | InkWell( 61 | onTap: () => 62 | OrgController.of(context).cycleVisibilityOf(section), 63 | onLongPress: () => 64 | OrgEvents.of(context).onSectionLongPress?.call(section), 65 | child: OrgHeadlineWidget( 66 | section.headline, 67 | open: _openEnough(visibility), 68 | highlighted: 69 | OrgController.of(context).sparseQuery?.matches(section), 70 | ), 71 | ), 72 | AnimatedSwitcher( 73 | duration: const Duration(milliseconds: 100), 74 | transitionBuilder: (child, animation) => 75 | SizeTransition(sizeFactor: animation, child: child), 76 | child: Column( 77 | key: ValueKey(visibility), 78 | crossAxisAlignment: CrossAxisAlignment.stretch, 79 | children: [ 80 | if (section.content != null && 81 | (visibility == OrgVisibilityState.children || 82 | visibility == OrgVisibilityState.subtree)) 83 | ..._contentWidgets(context), 84 | if (visibility != OrgVisibilityState.folded) 85 | ...section.sections 86 | .map((child) => OrgSectionWidget(child)), 87 | ], 88 | ), 89 | ), 90 | if (root) listBottomSafeArea(), 91 | ], 92 | ), 93 | ); 94 | return _withSlideActions(context, widget); 95 | } 96 | 97 | Iterable _contentWidgets(BuildContext context) sync* { 98 | for (final child in section.content!.children) { 99 | Widget widget = OrgContentWidget(child); 100 | final textDirection = OrgSettings.of(context).settings.textDirection ?? 101 | child.detectTextDirection(); 102 | if (textDirection != null) { 103 | widget = Directionality(textDirection: textDirection, child: widget); 104 | } 105 | yield widget; 106 | } 107 | } 108 | 109 | Widget _withSlideActions(BuildContext context, Widget child) { 110 | final actions = OrgEvents.of(context).onSectionSlide?.call(section); 111 | if (actions == null) return child; 112 | return Slidable( 113 | endActionPane: ActionPane( 114 | motion: const ScrollMotion(), 115 | children: actions, 116 | ), 117 | child: child, 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/src/widget/org_sub_superscript.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/widget/org_content.dart'; 3 | import 'package:org_parser/org_parser.dart'; 4 | 5 | const _kSubSuperScriptScale = 0.7; 6 | 7 | /// An Org superscript 8 | class OrgSuperscriptWidget extends StatelessWidget { 9 | const OrgSuperscriptWidget(this.superscript, {super.key}); 10 | 11 | final OrgSuperscript superscript; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final style = DefaultTextStyle.of(context).style; 16 | return DefaultTextStyle( 17 | style: style.copyWith( 18 | fontSize: style.fontSize! * _kSubSuperScriptScale, 19 | decoration: TextDecoration.none, 20 | ), 21 | child: Transform.translate( 22 | offset: Offset(0, style.fontSize! * -0.5), 23 | child: OrgContentWidget(superscript.body), 24 | ), 25 | ); 26 | } 27 | } 28 | 29 | /// An Org subscript 30 | class OrgSubscriptWidget extends StatelessWidget { 31 | const OrgSubscriptWidget(this.subscript, {super.key}); 32 | 33 | final OrgSubscript subscript; 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | final style = DefaultTextStyle.of(context).style; 38 | return DefaultTextStyle( 39 | style: style.copyWith( 40 | fontSize: style.fontSize! * _kSubSuperScriptScale, 41 | decoration: TextDecoration.none, 42 | ), 43 | child: Transform.translate( 44 | offset: Offset(0, style.fontSize! * 0.3), 45 | child: OrgContentWidget(subscript.body), 46 | ), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/widget/org_table.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/indent.dart'; 3 | import 'package:org_flutter/src/util/util.dart'; 4 | import 'package:org_flutter/src/widget/org_content.dart'; 5 | import 'package:org_flutter/src/widget/org_theme.dart'; 6 | import 'package:org_parser/org_parser.dart'; 7 | 8 | /// An Org Mode table 9 | class OrgTableWidget extends StatelessWidget { 10 | const OrgTableWidget(this.table, {super.key}); 11 | final OrgTable table; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return DefaultTextStyle.merge( 16 | style: TextStyle(color: OrgTheme.dataOf(context).tableColor), 17 | child: ConstrainedBox( 18 | // Ensure that table takes up entire width (can't have tables 19 | // side-by-side) 20 | constraints: const BoxConstraints.tightFor(width: double.infinity), 21 | child: Column( 22 | crossAxisAlignment: CrossAxisAlignment.start, 23 | children: [ 24 | SingleChildScrollView( 25 | scrollDirection: Axis.horizontal, 26 | physics: const AlwaysScrollableScrollPhysics(), 27 | child: IndentBuilder( 28 | table.indent, 29 | expanded: false, 30 | builder: (context, _) => _buildTable(context), 31 | ), 32 | ), 33 | if (table.trailing.isNotEmpty) 34 | Text(removeTrailingLineBreak(table.trailing)), 35 | ], 36 | ), 37 | ), 38 | ); 39 | } 40 | 41 | Widget _buildTable(BuildContext context) { 42 | final tableColor = OrgTheme.dataOf(context).tableColor; 43 | final borderSide = 44 | tableColor == null ? const BorderSide() : BorderSide(color: tableColor); 45 | return Table( 46 | defaultColumnWidth: const IntrinsicColumnWidth(), 47 | defaultVerticalAlignment: TableCellVerticalAlignment.baseline, 48 | textBaseline: TextBaseline.alphabetic, 49 | border: TableBorder( 50 | verticalInside: borderSide, 51 | left: borderSide, 52 | right: table.rectangular ? borderSide : BorderSide.none, 53 | ), 54 | children: _tableRows(borderSide).toList(growable: false), 55 | ); 56 | } 57 | 58 | Iterable _tableRows(BorderSide borderSide) sync* { 59 | final columnCount = table.columnCount; 60 | final numerical = List.generate(columnCount, table.columnIsNumeric); 61 | for (var i = 0; i < table.rows.length; i++) { 62 | final prevRow = i > 0 ? table.rows[i - 1] : null; 63 | final row = table.rows[i]; 64 | final nextRow = i + 1 < table.rows.length ? table.rows[i + 1] : null; 65 | if (row is OrgTableCellRow) { 66 | // Peek at surrounding rows, add borders for dividers 67 | final topBorder = 68 | i == 1 && prevRow is OrgTableDividerRow ? borderSide : null; 69 | final bottomBorder = nextRow is OrgTableDividerRow ? borderSide : null; 70 | final decoration = topBorder != null || bottomBorder != null 71 | ? BoxDecoration( 72 | border: Border( 73 | top: topBorder ?? BorderSide.none, 74 | bottom: bottomBorder ?? BorderSide.none, 75 | )) 76 | : null; 77 | yield TableRow( 78 | decoration: decoration, 79 | children: [ 80 | for (var j = 0; j < columnCount; j++) 81 | Padding( 82 | padding: const EdgeInsets.symmetric(horizontal: 16), 83 | child: j < row.cellCount 84 | ? OrgContentWidget( 85 | row.cells[j].content, 86 | textAlign: numerical[j] ? TextAlign.right : null, 87 | ) 88 | : const SizedBox.shrink(), 89 | ), 90 | ], 91 | ); 92 | } else if (prevRow is OrgTableDividerRow && row is OrgTableDividerRow) { 93 | yield TableRow( 94 | decoration: BoxDecoration(border: Border(bottom: borderSide)), 95 | children: List.filled(columnCount, const SizedBox(height: 8)), 96 | ); 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/widget/org_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:org_flutter/src/theme.dart'; 3 | 4 | /// The theme for the Org Mode document 5 | class OrgTheme extends InheritedWidget { 6 | const OrgTheme({ 7 | required super.child, 8 | required this.light, 9 | required this.dark, 10 | super.key, 11 | }); 12 | 13 | static OrgTheme of(BuildContext context) => 14 | context.dependOnInheritedWidgetOfExactType()!; 15 | 16 | /// Throws an exception if OrgTheme is not found in the context. 17 | static OrgThemeData dataOf(BuildContext context) { 18 | final theme = of(context); 19 | final brightness = Theme.of(context).brightness; 20 | switch (brightness) { 21 | case Brightness.dark: 22 | return theme.dark; 23 | case Brightness.light: 24 | return theme.light; 25 | } 26 | } 27 | 28 | final OrgThemeData light; 29 | final OrgThemeData dark; 30 | 31 | @override 32 | bool updateShouldNotify(OrgTheme oldWidget) => 33 | light != oldWidget.light || dark != oldWidget.dark; 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/widgets.dart: -------------------------------------------------------------------------------- 1 | export './widget/org_block.dart'; 2 | export './widget/org_comment.dart'; 3 | export './widget/org_content.dart'; 4 | export './widget/org_decrypted_content.dart'; 5 | export './widget/org_document.dart'; 6 | export './widget/org_drawer.dart'; 7 | export './widget/org_dynamic_block.dart'; 8 | export './widget/org_fixed_width_area.dart'; 9 | export './widget/org_footnote_reference.dart'; 10 | export './widget/org_headline.dart'; 11 | export './widget/org_horizontal_rule.dart'; 12 | export './widget/org_inline_src_block.dart'; 13 | export './widget/org_latex_block.dart'; 14 | export './widget/org_latex_inline.dart'; 15 | export './widget/org_link.dart'; 16 | export './widget/org_link_target.dart'; 17 | export './widget/org_list.dart'; 18 | export './widget/org_local_variables.dart'; 19 | export './widget/org_meta.dart'; 20 | export './widget/org_paragraph.dart'; 21 | export './widget/org_pgp_block.dart'; 22 | export './widget/org_property.dart'; 23 | export './widget/org_radio_target.dart'; 24 | export './widget/org_root.dart'; 25 | export './widget/org_section.dart'; 26 | export './widget/org_sub_superscript.dart'; 27 | export './widget/org_table.dart'; 28 | export './widget/org_theme.dart'; 29 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: org_flutter 2 | description: Flutter widgets for displaying Emacs Org Mode (https://orgmode.org) content 3 | version: 9.7.0 4 | homepage: https://github.com/amake/org_flutter 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | flutter: ">=3.27.0-0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | # flutter_highlighting not needed until pending issues addressed upstream; see 14 | # highlight.dart 15 | # flutter_highlighting: ^0.9.0+11.8.0 16 | flutter_slidable: ^4.0.0 17 | flutter_tex_js: ^5.0.0 18 | # flutter_tex_js: 19 | # path: ../flutter_tex_js/flutter_tex_js 20 | highlighting: ^0.9.0+11.8.0 21 | org_parser: ^9.6.0 22 | # org_parser: 23 | # path: ../org_parser 24 | petitparser: ^6.0.1 25 | petit_lisp: ">=6.4.0 <6.5.0" 26 | # petit_lisp: 27 | # path: ../petit_lisp 28 | more: ^4.4.0 29 | 30 | dev_dependencies: 31 | flutter_lints: ^5.0.0 32 | flutter_test: 33 | sdk: flutter 34 | -------------------------------------------------------------------------------- /test/alignment_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/src/util/util.dart'; 3 | import 'package:org_parser/org_parser.dart'; 4 | 5 | void main() { 6 | group('extract', () { 7 | test('simple', () { 8 | final doc = OrgDocument.parse(''' 9 | #+ATTR_ORG: :align center 10 | [[foo]] 11 | '''); 12 | final node = doc.find((_) => true)!.node; 13 | expect(alignmentForNode(node, doc), OrgAlignment.center); 14 | }); 15 | test('center', () { 16 | final doc = OrgDocument.parse(''' 17 | #+ATTR_ORG: :center t 18 | [[foo]] 19 | '''); 20 | final node = doc.find((_) => true)!.node; 21 | expect(alignmentForNode(node, doc), OrgAlignment.center); 22 | }); 23 | test('non-authoritative', () { 24 | final doc = OrgDocument.parse(''' 25 | #+ATTR_HTML: :align center 26 | [[foo]] 27 | '''); 28 | final node = doc.find((_) => true)!.node; 29 | expect(alignmentForNode(node, doc), OrgAlignment.center); 30 | }); 31 | test('authoritative override', () { 32 | final doc = OrgDocument.parse(''' 33 | #+ATTR_HTML: :align center 34 | #+ATTR_ORG: :align right 35 | [[foo]] 36 | '''); 37 | final node = doc.find((_) => true)!.node; 38 | expect(alignmentForNode(node, doc), OrgAlignment.right); 39 | }); 40 | test('case-insensitive', () { 41 | final doc = OrgDocument.parse(''' 42 | #+attr_org: :ALIGN center 43 | [[foo]] 44 | '''); 45 | final node = doc.find((_) => true)!.node; 46 | expect(alignmentForNode(node, doc), OrgAlignment.center); 47 | }); 48 | test('distant', () { 49 | final doc = OrgDocument.parse(''' 50 | #+ATTR_ORG: :align center 51 | #+ATTR_HTML: :foo bar 52 | #+ATTR_LATEX: :baz bazinga 53 | [[foo]] 54 | '''); 55 | final node = doc.find((_) => true)!.node; 56 | expect(alignmentForNode(node, doc), OrgAlignment.center); 57 | }); 58 | test('not present', () { 59 | final doc = OrgDocument.parse(''' 60 | [[foo]] 61 | '''); 62 | final node = doc.find((_) => true)!.node; 63 | expect(alignmentForNode(node, doc), isNull); 64 | }); 65 | test('invalid align value', () { 66 | final doc = OrgDocument.parse(''' 67 | #+ATTR_ORG: :align foo 68 | [[foo]] 69 | '''); 70 | final node = doc.find((_) => true)!.node; 71 | expect(alignmentForNode(node, doc), isNull); 72 | }); 73 | test('invalid center value', () { 74 | final doc = OrgDocument.parse(''' 75 | #+ATTR_ORG: :center foo 76 | [[foo]] 77 | '''); 78 | final node = doc.find((_) => true)!.node; 79 | expect(alignmentForNode(node, doc), isNull); 80 | }); 81 | test('missing plist value', () { 82 | final doc = OrgDocument.parse(''' 83 | #+ATTR_ORG: :align 84 | [[foo]] 85 | '''); 86 | final node = doc.find((_) => true)!.node; 87 | expect(alignmentForNode(node, doc), isNull); 88 | }); 89 | test('missing meta value', () { 90 | final doc = OrgDocument.parse(''' 91 | #+ATTR_ORG: 92 | [[foo]] 93 | '''); 94 | final node = doc.find((_) => true)!.node; 95 | expect(alignmentForNode(node, doc), isNull); 96 | }); 97 | test('in paragraph', () { 98 | final doc = OrgDocument.parse(''' 99 | #+ATTR_ORG: :align center 100 | a [[foo]] b 101 | '''); 102 | final node = doc.find((_) => true)!.node; 103 | expect(alignmentForNode(node, doc), isNull); 104 | }); 105 | }); 106 | group('plist', () { 107 | test('tokenize', () { 108 | expect( 109 | tokenizePlist(':align center'), 110 | [':align', 'center'], 111 | ); 112 | }); 113 | test('extra whitespace', () { 114 | expect( 115 | tokenizePlist(':align center'), 116 | [':align', 'center'], 117 | ); 118 | }); 119 | test('get value', () { 120 | expect( 121 | tokenizePlist(':align center').get(':align'), 122 | 'center', 123 | ); 124 | }); 125 | test('get missing key', () { 126 | expect( 127 | tokenizePlist(':align center').get(':foo'), 128 | isNull, 129 | ); 130 | }); 131 | test('malformed', () { 132 | expect( 133 | tokenizePlist(':align foo bar').get('bar'), 134 | isNull, 135 | ); 136 | }); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /test/bidi_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:org_flutter/src/util/bidi.dart'; 4 | import 'package:org_parser/org_parser.dart'; 5 | 6 | void main() { 7 | test('detect ltr', () { 8 | final node = OrgPlainText('foo'); 9 | final textDirection = node.detectTextDirection(); 10 | expect(textDirection, TextDirection.ltr); 11 | }); 12 | test('detect rtl', () { 13 | final node = OrgPlainText('אבج'); 14 | final textDirection = node.detectTextDirection(); 15 | expect(textDirection, TextDirection.rtl); 16 | }); 17 | test('detect unknown', () { 18 | final node = OrgPlainText('123'); 19 | final textDirection = node.detectTextDirection(); 20 | expect(textDirection, isNull); 21 | }); 22 | test('detect forced rtl', () { 23 | final node = OrgPlainText('\u200f123'); 24 | final textDirection = node.detectTextDirection(); 25 | expect(textDirection, TextDirection.rtl); 26 | }); 27 | test('detect forced ltr', () { 28 | final node = OrgPlainText('\u200e123'); 29 | final textDirection = node.detectTextDirection(); 30 | expect(textDirection, TextDirection.ltr); 31 | }); 32 | test('astral plane', () { 33 | final node = OrgPlainText('🌍'); 34 | final textDirection = node.detectTextDirection(); 35 | expect(textDirection, isNull); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/elisp_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/src/util/elisp.dart'; 3 | import 'package:petit_lisp/lisp.dart'; 4 | 5 | dynamic exec(String script, [InterruptCallback? interrupt]) { 6 | final env = ElispEnvironment(StandardEnvironment(NativeEnvironment())) 7 | ..interrupt = interrupt; 8 | return evalString(elispParser, env, script); 9 | } 10 | 11 | void main() { 12 | test('function quote', () { 13 | expect( 14 | elispParser.parse("#'foo").value, 15 | [Cons(Name('quote'), Cons(Name('foo')))], 16 | ); 17 | }); 18 | test('eq', () { 19 | expect(exec('(eq 1 1)'), true); 20 | expect(exec('(eq 1 2)'), false); 21 | expect(exec('(eq 1 1.0)'), false); 22 | expect(exec('(eq 1.0 1.0)'), true); 23 | expect(exec('(eq "foo" "foo")'), false); 24 | expect(exec('(eq "foo" "bar")'), false); 25 | expect(exec("(eq 'foo 'foo)"), true); 26 | }); 27 | test('lambda', () { 28 | expect(exec('((lambda (x &optional y) x) 1)'), 1); 29 | expect(exec('((lambda (x &optional y) y) 1)'), isNull); 30 | expect(exec('((lambda (x &optional y) y) 1 2)'), 2); 31 | }); 32 | test('add-to-list', () { 33 | expect(exec("(define foo null) (add-to-list 'foo 1)"), Cons(1)); 34 | expect(exec("(define foo '(1)) (add-to-list 'foo 1)"), Cons(1)); 35 | expect(exec("(define foo '(1)) (add-to-list 'foo 2)"), Cons(2, Cons(1))); 36 | expect(exec("(define foo '(1)) (add-to-list 'foo 2 t)"), Cons(1, Cons(2))); 37 | expect( 38 | exec("(define foo '(1)) (add-to-list 'foo 2 'foo)"), 39 | Cons(1, Cons(2)), 40 | ); 41 | expect( 42 | exec('''(define foo '("foo")) (add-to-list 'foo "foo" nil 'equal)'''), 43 | Cons("foo"), 44 | ); 45 | expect( 46 | exec('''(define foo '("foo")) (add-to-list 'foo "foo" nil 'eq)'''), 47 | Cons("foo", Cons("foo")), 48 | ); 49 | expect( 50 | exec('''(define foo '("foo")) (add-to-list 'foo "foo" nil #'eq)'''), 51 | Cons("foo", Cons("foo")), 52 | ); 53 | }); 54 | test('setq', () { 55 | expect(exec('(setq foo 1 bar 2) (cons foo bar)'), Cons(1, 2)); 56 | expect(exec('(setq foo 1 bar 2)'), 2); 57 | expect(exec('(define (foo) (setq bar 1)) (foo) bar'), 1); 58 | expect( 59 | () => exec('(define (foo x) (setq x 1)) (foo 0) x'), 60 | throwsArgumentError, 61 | ); 62 | }); 63 | test('dolist', () { 64 | expect( 65 | exec(""" 66 | (define result 0) 67 | (dolist (x '(1 2 3)) (setq result (+ result x))) 68 | result 69 | """), 70 | 6, 71 | ); 72 | expect( 73 | exec("(dolist (x '(1 2 3) result) (setq result (cons x result)))"), 74 | Cons(3, Cons(2, Cons(1))), 75 | ); 76 | }); 77 | test('defun', () { 78 | expect(exec('(defun foo () 1) (foo)'), 1); 79 | expect(exec('(defun bar (x) (+ 1 x)) (bar 1)'), 2); 80 | }); 81 | test('defvar', () { 82 | expect(exec('(defvar foo 1) foo'), 1); 83 | expect(exec('(defvar foo 1) (defvar foo 2) foo'), 2); 84 | }); 85 | test('defmacro', () { 86 | expect( 87 | exec("(defmacro foo (x) `(list 'x 'was (quote ,x))) (foo a)"), 88 | Cons(Name('x'), Cons(Name('was'), Cons(Name('a')))), 89 | ); 90 | }); 91 | test('infinite loop', () { 92 | final start = DateTime.timestamp().millisecondsSinceEpoch; 93 | expect( 94 | () => exec('(while t)', () { 95 | final now = DateTime.timestamp().millisecondsSinceEpoch; 96 | if (now - start > 200) throw StateError('interrupted'); 97 | }), 98 | throwsStateError, 99 | ); 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /test/locale_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:org_flutter/src/util/util.dart'; 4 | import 'package:org_parser/org_parser.dart'; 5 | 6 | void main() { 7 | group('extract', () { 8 | test('simple', () { 9 | final doc = OrgDocument.parse(''' 10 | #+LANGUAGE: en 11 | foo 12 | '''); 13 | expect(extractLocale(doc), Locale('en')); 14 | }); 15 | test('case-insensitive', () { 16 | final doc = OrgDocument.parse(''' 17 | #+language: en 18 | foo 19 | '''); 20 | expect(extractLocale(doc), Locale('en')); 21 | }); 22 | test('empty', () { 23 | final doc = OrgDocument.parse(''' 24 | foo 25 | '''); 26 | expect(extractLocale(doc), isNull); 27 | }); 28 | }); 29 | group('parse', () { 30 | test('language', () { 31 | expect( 32 | tryParseLocale('en'), 33 | Locale('en'), 34 | ); 35 | }); 36 | test('language and region', () { 37 | expect( 38 | tryParseLocale('en_US'), 39 | Locale('en', 'US'), 40 | ); 41 | }); 42 | test('language and script', () { 43 | expect( 44 | tryParseLocale('zh_Hans'), 45 | Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), 46 | ); 47 | }); 48 | test('language, script, and region', () { 49 | expect( 50 | tryParseLocale('zh_Hans_CN'), 51 | Locale.fromSubtags( 52 | languageCode: 'zh', 53 | scriptCode: 'Hans', 54 | countryCode: 'CN', 55 | ), 56 | ); 57 | }); 58 | test('hyphens', () { 59 | expect( 60 | tryParseLocale('zh-Hans-CN'), 61 | Locale.fromSubtags( 62 | languageCode: 'zh', 63 | scriptCode: 'Hans', 64 | countryCode: 'CN', 65 | ), 66 | ); 67 | }); 68 | group('invalid', () { 69 | test('empty', () { 70 | expect( 71 | tryParseLocale(''), 72 | isNull, 73 | ); 74 | }); 75 | test('too many parts', () { 76 | expect( 77 | tryParseLocale('en_Latn_US_foo'), 78 | isNull, 79 | ); 80 | }); 81 | test('second part bad length', () { 82 | expect( 83 | tryParseLocale('en_Lat'), 84 | isNull, 85 | ); 86 | }); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /test/text_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/src/util/text.dart'; 3 | 4 | void main() { 5 | group('detect indent', () { 6 | test('detect empty', () { 7 | final text = ''; 8 | expect(detectIndent(text), 0); 9 | }); 10 | test('detect none', () { 11 | final text = 'foo'; 12 | expect(detectIndent(text), 0); 13 | }); 14 | test('detect single line', () { 15 | final text = ' foo'; 16 | expect(detectIndent(text), 2); 17 | }); 18 | test('detect multiple lines', () { 19 | final text = ''' foo 20 | bar'''; 21 | expect(detectIndent(text), 2); 22 | }); 23 | test('middle blank line', () { 24 | final text = ''' foo 25 | ${' '} 26 | bar'''; 27 | expect(detectIndent(text), 2); 28 | }); 29 | test('trailing blank line', () { 30 | final text = ''' foo 31 | '''; 32 | expect(detectIndent(text), 2); 33 | }); 34 | test('only blank line', () { 35 | final text = ''' '''; 36 | expect(detectIndent(text), 0); 37 | }); 38 | }); 39 | group('soft deindent', () { 40 | test('deindent none', () { 41 | final text = 'foo'; 42 | expect(softDeindent(text, 0), text); 43 | expect(softDeindent(text, 1), text); 44 | expect(softDeindent(text, 2), text); 45 | }); 46 | test('deindent single line', () { 47 | final text = ' foo'; 48 | expect(softDeindent(text, 0), text); 49 | expect(softDeindent(text, 1), ' foo'); 50 | expect(softDeindent(text, 2), 'foo'); 51 | expect(softDeindent(text, 3), 'foo'); 52 | }); 53 | test('deindent multiple lines', () { 54 | final text = ''' 55 | foo 56 | bar'''; 57 | expect(softDeindent(text, 0), text); 58 | expect(softDeindent(text, 1), ' foo\n bar'); 59 | expect(softDeindent(text, 2), 'foo\n bar'); 60 | expect(softDeindent(text, 3), 'foo\n bar'); 61 | }); 62 | }); 63 | group('hard deindent', () { 64 | test('deindent none', () { 65 | final text = 'foo'; 66 | expect(hardDeindent(text, 0), text); 67 | expect(hardDeindent(text, 1), text); 68 | expect(hardDeindent(text, 2), text); 69 | }); 70 | test('deindent single line', () { 71 | final text = ' foo'; 72 | expect(hardDeindent(text, 0), text); 73 | expect(hardDeindent(text, 1), ' foo'); 74 | expect(hardDeindent(text, 2), 'foo'); 75 | expect(hardDeindent(text, 3), ' foo'); 76 | }); 77 | test('deindent multiple lines', () { 78 | final text = ''' 79 | foo 80 | bar'''; 81 | expect(hardDeindent(text, 0), text); 82 | expect(hardDeindent(text, 1), ' foo\n bar'); 83 | expect(hardDeindent(text, 2), 'foo\n bar'); 84 | expect(hardDeindent(text, 3), ' foo\n bar'); 85 | }); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /test/widget/basic_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:org_flutter/org_flutter.dart'; 6 | 7 | import './util.dart'; 8 | 9 | void main() { 10 | test('create widget', () { 11 | const Org('foo'); 12 | }); 13 | group('Org widget', () { 14 | testWidgets('Simple', (tester) async { 15 | await tester.pumpWidget(wrap(const Org('foo bar'))); 16 | expect(find.text('foo bar'), findsOneWidget); 17 | }); 18 | testWidgets('Big', (tester) async { 19 | final markup = File('test/widget/org-manual.org').readAsStringSync(); 20 | await tester.pumpWidget(wrap(Org(markup))); 21 | expect(find.textContaining('The Org Manual'), findsOneWidget); 22 | }); 23 | testWidgets('Screenshot', (tester) async { 24 | final markup = File('test/widget/org-manual.org').readAsStringSync(); 25 | final key = ValueKey('test'); 26 | await tester.pumpWidget(SingleChildScrollView( 27 | child: RepaintBoundary( 28 | key: key, 29 | child: wrap( 30 | OrgText( 31 | markup, 32 | settings: OrgSettings( 33 | startupFolded: OrgVisibilityState.subtree, 34 | reflowText: true, 35 | ), 36 | ), 37 | ), 38 | ), 39 | )); 40 | const goldDir = String.fromEnvironment('GOLD_DIR', defaultValue: '.'); 41 | await expectLater( 42 | find.byKey(key), 43 | matchesGoldenFile('$goldDir/org-manual.png'), 44 | ); 45 | // This is very slow and only good for manual checks, so we skip it. Run 46 | // with `make screenshot`. 47 | }, skip: true); 48 | }); 49 | group('OrgText widget', () { 50 | testWidgets('Simple', (tester) async { 51 | await tester.pumpWidget(wrap(const OrgText('foo bar'))); 52 | expect(find.text('foo bar'), findsOneWidget); 53 | }); 54 | testWidgets('Big', (tester) async { 55 | final markup = File('test/widget/org-manual.org').readAsStringSync(); 56 | await tester.pumpWidget(wrap(OrgText(markup))); 57 | expect(find.textContaining('The Org Manual'), findsOneWidget); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/widget/entities_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('Entities', () { 8 | testWidgets('Custom', (tester) async { 9 | final doc = OrgDocument.parse(r''' 10 | foo \pineapple bar 11 | 12 | # Local Variables: 13 | # org-entities-user: (("pineapple" "[p]" nil "🍍" "[p]" "[p]" "🍍")) 14 | # End: 15 | '''); 16 | final widget = OrgController( 17 | root: doc, 18 | interpretEmbeddedSettings: true, 19 | errorHandler: (e) { 20 | fail(e.toString()); 21 | }, 22 | child: OrgRootWidget( 23 | child: OrgDocumentWidget(doc), 24 | ), 25 | ); 26 | await tester.pumpWidget(wrap(widget)); 27 | expect(find.textContaining('foo \u{1f34d} bar'), findsOneWidget); 28 | }); 29 | testWidgets('Enabled', (tester) async { 30 | final doc = OrgDocument.parse(r''' 31 | foo \smiley bar^{2} baz_buzz 32 | 33 | # Local Variables: 34 | # org-pretty-entities: t 35 | # End: 36 | '''); 37 | final widget = OrgController( 38 | root: doc, 39 | interpretEmbeddedSettings: true, 40 | errorHandler: (e) { 41 | fail(e.toString()); 42 | }, 43 | child: OrgRootWidget( 44 | child: OrgDocumentWidget(doc), 45 | ), 46 | ); 47 | await tester.pumpWidget(wrap(widget)); 48 | expect(find.textContaining('☺'), findsOneWidget); 49 | expect(find.textContaining('foo'), findsOneWidget); 50 | expect(find.textContaining('^{2}'), findsNothing); 51 | expect(find.textContaining('2'), findsOneWidget); 52 | expect(find.textContaining('baz_buzz'), findsNothing); 53 | expect(find.textContaining('baz'), findsOneWidget); 54 | expect(find.textContaining('buzz'), findsOneWidget); 55 | }); 56 | testWidgets('Disabled', (tester) async { 57 | final doc = OrgDocument.parse(r''' 58 | foo \smiley bar^{2} baz_buzz 59 | 60 | # Local Variables: 61 | # org-pretty-entities: nil 62 | # End: 63 | '''); 64 | final widget = OrgController( 65 | root: doc, 66 | interpretEmbeddedSettings: true, 67 | errorHandler: (e) { 68 | fail(e.toString()); 69 | }, 70 | child: OrgRootWidget( 71 | child: OrgDocumentWidget(doc), 72 | ), 73 | ); 74 | await tester.pumpWidget(wrap(widget)); 75 | expect(find.textContaining('☺'), findsNothing); 76 | expect(find.textContaining('foo'), findsOneWidget); 77 | expect(find.textContaining('^{2}'), findsOneWidget); 78 | expect(find.textContaining('baz_buzz'), findsOneWidget); 79 | }); 80 | testWidgets('Sub/superscripts disabled', (tester) async { 81 | final doc = OrgDocument.parse(r''' 82 | foo \smiley bar^{2} baz_buzz 83 | 84 | # Local Variables: 85 | # org-pretty-entities: t 86 | # org-pretty-entities-include-sub-superscripts: nil 87 | # End: 88 | '''); 89 | final widget = OrgController( 90 | root: doc, 91 | interpretEmbeddedSettings: true, 92 | errorHandler: (e) { 93 | fail(e.toString()); 94 | }, 95 | child: OrgRootWidget( 96 | child: OrgDocumentWidget(doc), 97 | ), 98 | ); 99 | await tester.pumpWidget(wrap(widget)); 100 | expect(find.textContaining('☺'), findsOneWidget); 101 | expect(find.textContaining('foo'), findsOneWidget); 102 | expect(find.textContaining('^{2}'), findsOneWidget); 103 | expect(find.textContaining('baz_buzz'), findsOneWidget); 104 | }); 105 | testWidgets('Sub/superscripts disabled', (tester) async { 106 | final doc = OrgDocument.parse(r''' 107 | foo \smiley bar^{2} baz_buzz 108 | 109 | # Local Variables: 110 | # org-pretty-entities: t 111 | # org-use-sub-superscripts: {} 112 | # End: 113 | '''); 114 | final widget = OrgController( 115 | root: doc, 116 | interpretEmbeddedSettings: true, 117 | errorHandler: (e) { 118 | fail(e.toString()); 119 | }, 120 | child: OrgRootWidget( 121 | child: OrgDocumentWidget(doc), 122 | ), 123 | ); 124 | await tester.pumpWidget(wrap(widget)); 125 | expect(find.textContaining('foo'), findsOneWidget); 126 | expect(find.textContaining('^{2}'), findsNothing); 127 | expect(find.textContaining('☺'), findsOneWidget); 128 | expect(find.textContaining('baz_buzz'), findsOneWidget); 129 | }); 130 | }); 131 | } 132 | 133 | /* Put a pagebreak here so Emacs doesn't bother us about the Local Variables 134 | lists in the tests 135 | 136 | */ 137 | -------------------------------------------------------------------------------- /test/widget/footnotes_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import 'util.dart'; 5 | 6 | void main() { 7 | group('Footnotes', () { 8 | testWidgets('Keys', (tester) async { 9 | await tester.pumpWidget(wrap(const Org(''' 10 | foo[fn:1] 11 | 12 | [fn:1] bar baz'''))); 13 | final locator = 14 | OrgLocator.of(tester.element(find.textContaining('foo')))!; 15 | expect(locator.footnoteKeys.value.length, 2); 16 | }); 17 | testWidgets('Visibility', (tester) async { 18 | await tester.pumpWidget(wrap(const Org(''' 19 | foo[fn:1] 20 | 21 | * bar baz 22 | [fn:1] bazinga'''))); 23 | expect(find.textContaining('bazinga'), findsNothing); 24 | await tester.tap(find.textContaining('fn:1').first); 25 | await tester.pumpAndSettle(); 26 | expect(find.textContaining('bazinga'), findsOneWidget); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/widget/headline_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('Not reflowed', () { 8 | group('Headline closed', () { 9 | testWidgets('Empty, no tags', (tester) async { 10 | await tester.pumpWidget(wrap(const Org('* foo bar'))); 11 | expect(find.textContaining('foo bar'), findsOneWidget); 12 | expect(find.textContaining('...'), findsNothing); 13 | }); 14 | testWidgets('Empty with tags', (tester) async { 15 | await tester.pumpWidget(wrap(const Org('* foo bar :tag:'))); 16 | expect(find.textContaining('foo bar'), findsOneWidget); 17 | expect(find.textContaining(':tag:'), findsOneWidget); 18 | expect(find.textContaining('...'), findsNothing); 19 | }); 20 | testWidgets('Contentful, no tags', (tester) async { 21 | await tester.pumpWidget(wrap(const Org('''* foo bar 22 | content'''))); 23 | expect(find.textContaining('foo bar'), findsOneWidget); 24 | expect(find.textContaining('content'), findsNothing); 25 | expect(find.textContaining('...'), findsOneWidget); 26 | }); 27 | testWidgets('Contentful with tags', (tester) async { 28 | await tester.pumpWidget(wrap(const Org('''* foo bar :tag: 29 | content'''))); 30 | expect(find.textContaining('foo bar'), findsOneWidget); 31 | expect(find.textContaining(':tag:'), findsOneWidget); 32 | expect(find.textContaining('content'), findsNothing); 33 | expect(find.textContaining('...'), findsOneWidget); 34 | }); 35 | }); 36 | group('Headline open', () { 37 | const settings = OrgSettings(startupFolded: OrgVisibilityState.subtree); 38 | testWidgets('Empty, no tags', (tester) async { 39 | await tester 40 | .pumpWidget(wrap(const Org('* foo bar', settings: settings))); 41 | expect(find.textContaining('foo bar'), findsOneWidget); 42 | expect(find.textContaining('...'), findsNothing); 43 | }); 44 | testWidgets('Empty with tags', (tester) async { 45 | await tester.pumpWidget( 46 | wrap(const Org('* foo bar :tag:', settings: settings))); 47 | expect(find.textContaining('foo bar'), findsOneWidget); 48 | expect(find.textContaining(':tag:'), findsOneWidget); 49 | expect(find.textContaining('...'), findsNothing); 50 | }); 51 | testWidgets('Contentful, no tags', (tester) async { 52 | await tester.pumpWidget(wrap(const Org('''* foo bar 53 | content''', settings: settings))); 54 | expect(find.textContaining('foo bar'), findsOneWidget); 55 | expect(find.textContaining('...'), findsNothing); 56 | expect(find.textContaining('content'), findsOneWidget); 57 | }); 58 | testWidgets('Contentful with tags', (tester) async { 59 | await tester.pumpWidget(wrap(const Org('''* foo bar :tag: 60 | content''', settings: settings))); 61 | expect(find.textContaining('foo bar'), findsOneWidget); 62 | expect(find.textContaining(':tag:'), findsOneWidget); 63 | expect(find.textContaining('content'), findsOneWidget); 64 | expect(find.textContaining('...'), findsNothing); 65 | }); 66 | }); 67 | }); 68 | group('Reflowed', () { 69 | const baseSettings = OrgSettings(reflowText: true); 70 | group('Headline closed', () { 71 | testWidgets('Empty, no tags', (tester) async { 72 | await tester 73 | .pumpWidget(wrap(const Org('* foo bar', settings: baseSettings))); 74 | expect(find.textContaining('foo bar'), findsOneWidget); 75 | expect(find.textContaining('...'), findsNothing); 76 | }); 77 | testWidgets('Empty with tags', (tester) async { 78 | await tester.pumpWidget( 79 | wrap(const Org('* foo bar :tag:', settings: baseSettings))); 80 | expect(find.textContaining('foo bar'), findsOneWidget); 81 | expect(find.textContaining(':tag:'), findsOneWidget); 82 | expect(find.textContaining('...'), findsNothing); 83 | }); 84 | testWidgets('Contentful, no tags', (tester) async { 85 | await tester.pumpWidget(wrap(const Org('''* foo bar 86 | content''', settings: baseSettings))); 87 | expect(find.textContaining('foo bar'), findsOneWidget); 88 | expect(find.textContaining('content'), findsNothing); 89 | expect(find.textContaining('...'), findsOneWidget); 90 | }); 91 | testWidgets('Contentful with tags', (tester) async { 92 | await tester.pumpWidget(wrap(const Org('''* foo bar :tag: 93 | content''', settings: baseSettings))); 94 | expect(find.textContaining('foo bar'), findsOneWidget); 95 | expect(find.textContaining(':tag:'), findsOneWidget); 96 | expect(find.textContaining('content'), findsNothing); 97 | expect(find.textContaining('...'), findsOneWidget); 98 | }); 99 | }); 100 | group('Headline open', () { 101 | final settings = 102 | baseSettings.copyWith(startupFolded: OrgVisibilityState.subtree); 103 | testWidgets('Empty, no tags', (tester) async { 104 | await tester.pumpWidget(wrap(Org('* foo bar', settings: settings))); 105 | expect(find.textContaining('foo bar'), findsOneWidget); 106 | expect(find.textContaining('...'), findsNothing); 107 | }); 108 | testWidgets('Empty with tags', (tester) async { 109 | await tester 110 | .pumpWidget(wrap(Org('* foo bar :tag:', settings: settings))); 111 | expect(find.textContaining('foo bar'), findsOneWidget); 112 | expect(find.textContaining(':tag:'), findsOneWidget); 113 | expect(find.textContaining('...'), findsNothing); 114 | }); 115 | testWidgets('Contentful, no tags', (tester) async { 116 | await tester.pumpWidget(wrap(Org('''* foo bar 117 | content''', settings: settings))); 118 | expect(find.textContaining('foo bar'), findsOneWidget); 119 | expect(find.textContaining('...'), findsNothing); 120 | expect(find.textContaining('content'), findsOneWidget); 121 | }); 122 | testWidgets('Contentful with tags', (tester) async { 123 | await tester.pumpWidget(wrap(Org('''* foo bar :tag: 124 | content''', settings: settings))); 125 | expect(find.textContaining('foo bar'), findsOneWidget); 126 | expect(find.textContaining(':tag:'), findsOneWidget); 127 | expect(find.textContaining('content'), findsOneWidget); 128 | expect(find.textContaining('...'), findsNothing); 129 | }); 130 | }); 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /test/widget/images_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('Images', () { 8 | testWidgets('Path link', (tester) async { 9 | var invoked = false; 10 | await tester.pumpWidget(wrap(Org( 11 | 'file:./foo.png', 12 | loadImage: (link) { 13 | invoked = true; 14 | expect(link.location, 'file:./foo.png'); 15 | return null; 16 | }, 17 | ))); 18 | expect(invoked, isTrue); 19 | }); 20 | testWidgets('Bracket link', (tester) async { 21 | var invoked = false; 22 | await tester.pumpWidget(wrap(Org( 23 | '[[file:./foo.png]]', 24 | loadImage: (link) { 25 | invoked = true; 26 | expect(link.location, 'file:./foo.png'); 27 | return null; 28 | }, 29 | ))); 30 | expect(invoked, isTrue); 31 | }); 32 | testWidgets('Bracket link with description', (tester) async { 33 | await tester.pumpWidget(wrap(Org( 34 | '[[file:./foo.png][foo]]', 35 | loadImage: (link) { 36 | fail('Should not be invoked'); 37 | }, 38 | ))); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/widget/keyword_settings_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('Keyword settings', () { 8 | testWidgets('Blocks start closed', (tester) async { 9 | final doc = OrgDocument.parse(r''' 10 | #+begin_example 11 | foo bar 12 | #+end_example 13 | 14 | #+STARTUP: hideblocks 15 | '''); 16 | final widget = OrgController( 17 | root: doc, 18 | interpretEmbeddedSettings: true, 19 | errorHandler: (e) { 20 | fail(e.toString()); 21 | }, 22 | child: OrgRootWidget( 23 | child: OrgDocumentWidget(doc), 24 | ), 25 | ); 26 | await tester.pumpWidget(wrap(widget)); 27 | expect(find.textContaining('foo bar'), findsNothing); 28 | }); 29 | testWidgets('Blocks start open', (tester) async { 30 | final doc = OrgDocument.parse(r''' 31 | #+begin_example 32 | foo bar 33 | #+end_example 34 | 35 | #+STARTUP: nohideblocks 36 | '''); 37 | final widget = OrgController( 38 | root: doc, 39 | interpretEmbeddedSettings: true, 40 | errorHandler: (e) { 41 | fail(e.toString()); 42 | }, 43 | child: OrgRootWidget( 44 | child: OrgDocumentWidget(doc), 45 | ), 46 | ); 47 | await tester.pumpWidget(wrap(widget)); 48 | expect(find.textContaining('foo bar'), findsOneWidget); 49 | }); 50 | testWidgets('Drawers start open', (tester) async { 51 | final doc = OrgDocument.parse(r''' 52 | :PROPERTIES: 53 | :foo: bar 54 | :END: 55 | 56 | #+STARTUP: nohidedrawers 57 | '''); 58 | final widget = OrgController( 59 | root: doc, 60 | interpretEmbeddedSettings: true, 61 | errorHandler: (e) { 62 | fail(e.toString()); 63 | }, 64 | child: OrgRootWidget( 65 | child: OrgDocumentWidget(doc), 66 | ), 67 | ); 68 | await tester.pumpWidget(wrap(widget)); 69 | expect(find.textContaining(':foo: bar'), findsOneWidget); 70 | }); 71 | testWidgets('Drawers start closed', (tester) async { 72 | final doc = OrgDocument.parse(r''' 73 | :PROPERTIES: 74 | :foo: bar 75 | :END: 76 | 77 | #+STARTUP: hidedrawers 78 | '''); 79 | final widget = OrgController( 80 | root: doc, 81 | interpretEmbeddedSettings: true, 82 | errorHandler: (e) { 83 | fail(e.toString()); 84 | }, 85 | child: OrgRootWidget( 86 | child: OrgDocumentWidget(doc), 87 | ), 88 | ); 89 | await tester.pumpWidget(wrap(widget)); 90 | expect(find.textContaining(':foo: bar'), findsNothing); 91 | }); 92 | testWidgets('Sections start open', (tester) async { 93 | final doc = OrgDocument.parse(r''' 94 | foo 95 | 96 | * bar 97 | baz 98 | #+STARTUP: showeverything 99 | '''); 100 | final widget = OrgController( 101 | root: doc, 102 | interpretEmbeddedSettings: true, 103 | errorHandler: (e) { 104 | fail(e.toString()); 105 | }, 106 | child: OrgRootWidget( 107 | child: OrgDocumentWidget(doc), 108 | ), 109 | ); 110 | await tester.pumpWidget(wrap(widget)); 111 | expect(find.textContaining('baz'), findsOneWidget); 112 | }); 113 | testWidgets('Sections start closed', (tester) async { 114 | final doc = OrgDocument.parse(r''' 115 | foo 116 | 117 | * bar 118 | baz 119 | #+STARTUP: overview 120 | '''); 121 | final widget = OrgController( 122 | root: doc, 123 | interpretEmbeddedSettings: true, 124 | errorHandler: (e) { 125 | fail(e.toString()); 126 | }, 127 | child: OrgRootWidget( 128 | child: OrgDocumentWidget(doc), 129 | ), 130 | ); 131 | await tester.pumpWidget(wrap(widget)); 132 | expect(find.textContaining('baz'), findsNothing); 133 | }); 134 | testWidgets('Showeverything overrides hideblocks, hidedrawers', 135 | (tester) async { 136 | final doc = OrgDocument.parse(r''' 137 | :PROPERTIES: 138 | :foo: bar 139 | :END: 140 | 141 | #+begin_example 142 | biz baz 143 | #+end_example 144 | 145 | #+STARTUP: showeverything hideblocks hidedrawers 146 | '''); 147 | final widget = OrgController( 148 | root: doc, 149 | interpretEmbeddedSettings: true, 150 | errorHandler: (e) { 151 | fail(e.toString()); 152 | }, 153 | child: OrgRootWidget( 154 | child: OrgDocumentWidget(doc), 155 | ), 156 | ); 157 | await tester.pumpWidget(wrap(widget)); 158 | expect(find.textContaining(':foo: bar'), findsOneWidget); 159 | expect(find.textContaining('biz baz'), findsOneWidget); 160 | }); 161 | testWidgets('Hide stars', (tester) async { 162 | final doc = OrgDocument.parse(r''' 163 | * foo 164 | ** bar 165 | *** baz 166 | 167 | #+STARTUP: showeverything hidestars 168 | '''); 169 | final widget = OrgController( 170 | root: doc, 171 | interpretEmbeddedSettings: true, 172 | errorHandler: (e) { 173 | fail(e.toString()); 174 | }, 175 | child: OrgRootWidget( 176 | child: OrgDocumentWidget(doc), 177 | ), 178 | ); 179 | await tester.pumpWidget(wrap(widget)); 180 | expect(find.textContaining('* foo'), findsOneWidget); 181 | expect(find.textContaining('** bar'), findsNothing); 182 | expect(find.textContaining(' * bar'), findsOneWidget); 183 | expect(find.textContaining('*** baz'), findsNothing); 184 | expect(find.textContaining(' * baz'), findsOneWidget); 185 | }); 186 | testWidgets('Disable entities', (tester) async { 187 | final doc = OrgDocument.parse(r''' 188 | foo \smiley bar^{2} baz_\alpha 189 | 190 | #+STARTUP: entitiesplain 191 | '''); 192 | final widget = OrgController( 193 | root: doc, 194 | interpretEmbeddedSettings: true, 195 | errorHandler: (e) { 196 | fail(e.toString()); 197 | }, 198 | child: OrgRootWidget( 199 | child: OrgDocumentWidget(doc), 200 | ), 201 | ); 202 | await tester.pumpWidget(wrap(widget)); 203 | expect(find.textContaining('foo'), findsOneWidget); 204 | expect(find.textContaining('^{2}'), findsOneWidget); 205 | expect(find.textContaining(r'_\alpha'), findsOneWidget); 206 | expect(find.textContaining('☺'), findsNothing); 207 | }); 208 | testWidgets('Disable inline images', (tester) async { 209 | final doc = OrgDocument.parse(r''' 210 | file:./foo.png 211 | 212 | #+STARTUP: noinlineimages 213 | '''); 214 | final widget = OrgController( 215 | root: doc, 216 | interpretEmbeddedSettings: true, 217 | errorHandler: (e) { 218 | fail(e.toString()); 219 | }, 220 | child: OrgRootWidget( 221 | loadImage: (_) => fail('Should not be invoked'), 222 | child: OrgDocumentWidget(doc), 223 | ), 224 | ); 225 | await tester.pumpWidget(wrap(widget)); 226 | }); 227 | }); 228 | } 229 | -------------------------------------------------------------------------------- /test/widget/link_target_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import 'util.dart'; 5 | 6 | void main() { 7 | group('Link target', () { 8 | testWidgets('Keys', (tester) async { 9 | await tester.pumpWidget(wrap(const Org(''' 10 | <> 11 | '''))); 12 | final locator = 13 | OrgLocator.of(tester.element(find.textContaining('foo')))!; 14 | expect(locator.linkTargetKeys.value.length, 1); 15 | }); 16 | testWidgets('Visibility', (tester) async { 17 | await tester.pumpWidget(wrap(const Org(''' 18 | * bar baz 19 | <>'''))); 20 | expect(find.textContaining('foo'), findsNothing); 21 | final locator = 22 | OrgLocator.of(tester.element(find.textContaining('bar')))!; 23 | locator.jumpToLinkTarget('foo'); 24 | await tester.pumpAndSettle(); 25 | expect(find.textContaining('foo'), findsOneWidget); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/widget/local_variables_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('Local variables', () { 8 | testWidgets('Hide markup', (tester) async { 9 | final doc = OrgDocument.parse(r''' 10 | foo *bar* baz 11 | 12 | # Local Variables: 13 | # org-hide-emphasis-markers: t 14 | # End: 15 | '''); 16 | final widget = OrgController( 17 | root: doc, 18 | interpretEmbeddedSettings: true, 19 | errorHandler: (e) { 20 | fail(e.toString()); 21 | }, 22 | child: OrgRootWidget( 23 | child: OrgDocumentWidget(doc), 24 | ), 25 | ); 26 | await tester.pumpWidget(wrap(widget)); 27 | expect(find.textContaining('foo bar baz'), findsOneWidget); 28 | expect(find.textContaining('foo *bar* baz'), findsNothing); 29 | }); 30 | }); 31 | } 32 | 33 | /* Put a pagebreak here so Emacs doesn't bother us about the Local Variables 34 | lists in the tests 35 | 36 | */ 37 | -------------------------------------------------------------------------------- /test/widget/meta_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('Meta', () { 8 | testWidgets('Non-exported', (tester) async { 9 | await tester 10 | .pumpWidget(wrap(const Org(r'#+FOO: foo^1 bar^{2} baz_3 buzz_{4}'))); 11 | expect(find.textContaining('^1'), findsOneWidget); 12 | expect(find.textContaining('2'), findsOneWidget); 13 | expect(find.textContaining('{2}'), findsNothing); 14 | expect(find.textContaining('_3'), findsOneWidget); 15 | expect(find.textContaining('4'), findsOneWidget); 16 | expect(find.textContaining('{4}'), findsNothing); 17 | }); 18 | testWidgets('Exported', (tester) async { 19 | await tester.pumpWidget( 20 | wrap(const Org(r'#+CAPTION: foo^1 bar^{2} baz_3 buzz_{4}'))); 21 | expect(find.textContaining('1'), findsOneWidget); 22 | expect(find.textContaining('^1'), findsNothing); 23 | expect(find.textContaining('2'), findsOneWidget); 24 | expect(find.textContaining('{2}'), findsNothing); 25 | expect(find.textContaining('3'), findsOneWidget); 26 | expect(find.textContaining('_3'), findsNothing); 27 | expect(find.textContaining('4'), findsOneWidget); 28 | expect(find.textContaining('{4}'), findsNothing); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/widget/named_element_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import 'util.dart'; 5 | 6 | void main() { 7 | group('Named element', () { 8 | testWidgets('Keys', (tester) async { 9 | await tester.pumpWidget(wrap(const Org(''' 10 | #+name: foo 11 | #+NAME: bar 12 | '''))); 13 | final locator = 14 | OrgLocator.of(tester.element(find.textContaining('foo')))!; 15 | expect(locator.nameKeys.value.length, 2); 16 | }); 17 | testWidgets('Visibility', (tester) async { 18 | await tester.pumpWidget(wrap(const Org(''' 19 | * bar baz 20 | #+NAME: foo'''))); 21 | expect(find.textContaining('foo'), findsNothing); 22 | final locator = 23 | OrgLocator.of(tester.element(find.textContaining('bar')))!; 24 | locator.jumpToName('foo'); 25 | await tester.pumpAndSettle(); 26 | expect(find.textContaining('foo'), findsOneWidget); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/widget/org_attach_id_dir_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('org-attach-id-dir', () { 8 | testWidgets('Detects valid', (tester) async { 9 | final doc = OrgDocument.parse(r''' 10 | foo *bar* baz 11 | 12 | # Local Variables: 13 | # org-attach-id-dir: "../foo" 14 | # End: 15 | '''); 16 | final widget = OrgController( 17 | root: doc, 18 | interpretEmbeddedSettings: true, 19 | errorHandler: (e) { 20 | fail(e.toString()); 21 | }, 22 | child: OrgRootWidget( 23 | child: OrgDocumentWidget(doc), 24 | ), 25 | ); 26 | await tester.pumpWidget(wrap(widget)); 27 | final settings = 28 | OrgSettings.of(tester.element(find.byType(OrgRootWidget))); 29 | expect(settings.settings.orgAttachIdDir, '../foo'); 30 | }); 31 | testWidgets('Ignores invalid', (tester) async { 32 | final doc = OrgDocument.parse(r''' 33 | foo *bar* baz 34 | 35 | # Local Variables: 36 | # org-attach-id-dir: foo 37 | # End: 38 | '''); 39 | final widget = OrgController( 40 | root: doc, 41 | interpretEmbeddedSettings: true, 42 | errorHandler: (e) { 43 | fail(e.toString()); 44 | }, 45 | child: OrgRootWidget( 46 | child: OrgDocumentWidget(doc), 47 | ), 48 | ); 49 | await tester.pumpWidget(wrap(widget)); 50 | final settings = 51 | OrgSettings.of(tester.element(find.byType(OrgRootWidget))); 52 | expect(settings.settings.orgAttachIdDir, 'data'); 53 | }); 54 | }); 55 | } 56 | 57 | /* Put a pagebreak here so Emacs doesn't bother us about the Local Variables 58 | lists in the tests 59 | 60 | */ 61 | -------------------------------------------------------------------------------- /test/widget/prettification_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | testWidgets('Pretty', (tester) async { 8 | await tester.pumpWidget(wrap(const Org(r'foo \smiley bar^{2} baz_\alpha'))); 9 | expect(find.textContaining('☺'), findsOneWidget); 10 | expect(find.textContaining(r'\smiley'), findsNothing); 11 | expect(find.textContaining('2'), findsOneWidget); 12 | expect(find.textContaining('{2}'), findsNothing); 13 | expect(find.textContaining('α'), findsOneWidget); 14 | expect(find.textContaining(r'\alpha'), findsNothing); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/widget/radio_link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import 'util.dart'; 5 | 6 | void main() { 7 | group('Radio links', () { 8 | testWidgets('Keys', (tester) async { 9 | final doc = 10 | OrgDocument.parse('<<>>', interpretEmbeddedSettings: true); 11 | final widget = OrgController( 12 | root: doc, 13 | errorHandler: (e) { 14 | fail(e.toString()); 15 | }, 16 | child: OrgLocator( 17 | child: OrgRootWidget( 18 | child: OrgDocumentWidget(doc), 19 | ), 20 | ), 21 | ); 22 | await tester.pumpWidget(wrap(widget)); 23 | final locator = 24 | OrgLocator.of(tester.element(find.textContaining('foo')))!; 25 | expect(locator.radioTargetKeys.value.length, 1); 26 | }); 27 | testWidgets('Visibility', (tester) async { 28 | final doc = OrgDocument.parse(''' 29 | FOO 30 | 31 | * bar baz 32 | bazinga 33 | <<>>''', interpretEmbeddedSettings: true); 34 | final widget = OrgController( 35 | root: doc, 36 | errorHandler: (e) { 37 | fail(e.toString()); 38 | }, 39 | child: OrgLocator( 40 | child: OrgRootWidget( 41 | child: OrgDocumentWidget(doc), 42 | ), 43 | ), 44 | ); 45 | await tester.pumpWidget(wrap(widget)); 46 | 47 | expect(find.textContaining('bazinga'), findsNothing); 48 | await tester.tapOnText(find.textRange.ofSubstring('FOO')); 49 | await tester.pumpAndSettle(); 50 | expect(find.textContaining('bazinga'), findsOneWidget); 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /test/widget/search_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | group('Search', () { 8 | group('Results', () { 9 | testWidgets('No query', (tester) async { 10 | final doc = OrgDocument.parse('foo bar baz'); 11 | final widget = OrgController( 12 | root: doc, 13 | child: OrgRootWidget( 14 | child: OrgDocumentWidget(doc), 15 | ), 16 | ); 17 | await tester.pumpWidget(wrap(widget)); 18 | final controller = OrgController.of( 19 | tester.element(find.textContaining('foo bar baz'))); 20 | expect(controller.searchResultKeys.value.length, 0); 21 | }); 22 | testWidgets('One result', (tester) async { 23 | final doc = OrgDocument.parse('foo bar baz'); 24 | final widget = OrgController( 25 | root: doc, 26 | searchQuery: 'bar', 27 | child: OrgRootWidget( 28 | child: OrgDocumentWidget(doc), 29 | ), 30 | ); 31 | await tester.pumpWidget(wrap(widget)); 32 | final controller = 33 | OrgController.of(tester.element(find.textContaining('foo'))); 34 | expect(controller.searchResultKeys.value.length, 1); 35 | }); 36 | testWidgets('Multiple results', (tester) async { 37 | final doc = OrgDocument.parse('foo bar baz'); 38 | final widget = OrgController( 39 | root: doc, 40 | searchQuery: RegExp('ba[rz]'), 41 | child: OrgRootWidget( 42 | child: OrgDocumentWidget(doc), 43 | ), 44 | ); 45 | await tester.pumpWidget(wrap(widget)); 46 | final controller = 47 | OrgController.of(tester.element(find.textContaining('foo'))); 48 | expect(controller.searchResultKeys.value.length, 2); 49 | }); 50 | }); 51 | group('Visibility', () { 52 | testWidgets('No query', (tester) async { 53 | final doc = OrgDocument.parse('''foo1 54 | * bar 55 | foo2 56 | ** baz 57 | foo3'''); 58 | final widget = OrgController( 59 | root: doc, 60 | child: OrgRootWidget( 61 | child: OrgDocumentWidget(doc), 62 | ), 63 | ); 64 | await tester.pumpWidget(wrap(widget)); 65 | expect(find.textContaining('foo1'), findsOneWidget); 66 | expect(find.textContaining('foo2'), findsNothing); 67 | expect(find.textContaining('foo3'), findsNothing); 68 | }); 69 | testWidgets('Nested hits', (tester) async { 70 | final doc = OrgDocument.parse('''foo1 71 | * bar 72 | foo2 73 | ** baz 74 | foo3'''); 75 | final widget = OrgController( 76 | root: doc, 77 | searchQuery: RegExp('foo[123]'), 78 | child: OrgRootWidget( 79 | child: OrgDocumentWidget(doc), 80 | ), 81 | ); 82 | await tester.pumpWidget(wrap(widget)); 83 | expect(find.textContaining('foo1'), findsOneWidget); 84 | expect(find.textContaining('foo2'), findsOneWidget); 85 | expect(find.textContaining('foo3'), findsOneWidget); 86 | }); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /test/widget/section_matcher_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import 'util.dart'; 5 | 6 | void main() { 7 | group('Section matcher', () { 8 | testWidgets('Keyword', (tester) async { 9 | final doc = OrgDocument.parse('''foo1 10 | * foo 11 | ** TODO bar 12 | foo2 13 | ** DONE baz 14 | foo3'''); 15 | final widget = OrgController( 16 | root: doc, 17 | sparseQuery: OrgQueryMatcher.fromMarkup('TODO="TODO"'), 18 | child: OrgRootWidget( 19 | child: OrgDocumentWidget(doc), 20 | ), 21 | ); 22 | await tester.pumpWidget(wrap(widget)); 23 | expect(find.textContaining('foo1'), findsOneWidget); 24 | expect(find.textContaining('bar'), findsOneWidget); 25 | expect(find.textContaining('foo2'), findsNothing); 26 | expect(find.textContaining('foo3'), findsNothing); 27 | }); 28 | testWidgets('Tag', (tester) async { 29 | final doc = OrgDocument.parse('''foo1 30 | * foo 31 | ** bar 32 | foo2 33 | ** baz :buzz: 34 | foo3'''); 35 | final widget = OrgController( 36 | root: doc, 37 | sparseQuery: const OrgQueryTagMatcher('buzz'), 38 | child: OrgRootWidget( 39 | child: OrgDocumentWidget(doc), 40 | ), 41 | ); 42 | await tester.pumpWidget(wrap(widget)); 43 | expect(find.textContaining('foo1'), findsOneWidget); 44 | expect(find.textContaining('foo2'), findsNothing); 45 | expect(find.textContaining('baz'), findsOneWidget); 46 | expect(find.textContaining('foo3'), findsNothing); 47 | }); 48 | group('With search', () { 49 | testWidgets('With hit', (tester) async { 50 | final doc = OrgDocument.parse('''foo1 51 | * foo 52 | ** TODO bar 53 | foo2 54 | ** DONE baz 55 | foo3'''); 56 | final widget = OrgController( 57 | root: doc, 58 | sparseQuery: OrgQueryMatcher.fromMarkup('TODO="TODO"'), 59 | searchQuery: 'foo2', 60 | child: OrgRootWidget( 61 | child: OrgDocumentWidget(doc), 62 | ), 63 | ); 64 | await tester.pumpWidget(wrap(widget)); 65 | expect(find.textContaining('foo1'), findsOneWidget); 66 | expect(find.textContaining('bar'), findsOneWidget); 67 | expect(find.textContaining('foo2'), findsOneWidget); 68 | expect(find.textContaining('foo3'), findsNothing); 69 | }); 70 | testWidgets('No hits', (tester) async { 71 | final doc = OrgDocument.parse('''foo1 72 | * foo 73 | ** TODO bar 74 | foo2 75 | ** DONE baz 76 | foo3'''); 77 | final widget = OrgController( 78 | root: doc, 79 | sparseQuery: OrgQueryMatcher.fromMarkup('TODO="TODO"'), 80 | searchQuery: 'foo3', 81 | child: OrgRootWidget( 82 | child: OrgDocumentWidget(doc), 83 | ), 84 | ); 85 | await tester.pumpWidget(wrap(widget)); 86 | expect(find.textContaining('foo1'), findsOneWidget); 87 | expect(find.textContaining('foo2'), findsNothing); 88 | expect(find.textContaining('foo3'), findsNothing); 89 | }); 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /test/widget/state_restoration_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:org_flutter/org_flutter.dart'; 4 | 5 | import './util.dart'; 6 | 7 | void main() { 8 | group('State restoration', () { 9 | testWidgets('No restoration ID', (tester) async { 10 | await tester.pumpWidget(RootRestorationScope( 11 | restorationId: 'root', 12 | child: wrap(const Org(''' 13 | foo bar 14 | * headline 1 15 | baz buzz 16 | ** headline 2 17 | bazinga''')), 18 | )); 19 | expect(find.text('foo bar'), findsOneWidget); 20 | await tester.tap(find.byType(OrgHeadlineWidget).first); 21 | await tester.pump(); 22 | expect(find.text('foo bar'), findsOneWidget); 23 | expect(find.text('baz buzz'), findsOneWidget); 24 | expect(find.textContaining('headline 2'), findsOneWidget); 25 | expect(find.text('bazinga'), findsNothing); 26 | await tester.restartAndRestore(); 27 | expect(find.text('foo bar'), findsOneWidget); 28 | expect(find.text('baz buzz'), findsNothing); 29 | expect(find.textContaining('headline 2'), findsNothing); 30 | expect(find.text('bazinga'), findsNothing); 31 | }); 32 | testWidgets('Restores section visibility', (tester) async { 33 | await tester.pumpWidget(RootRestorationScope( 34 | restorationId: 'root', 35 | child: wrap(const Org( 36 | ''' 37 | foo bar 38 | * headline 1 39 | baz buzz 40 | ** headline 2 41 | bazinga''', 42 | restorationId: 'doc', 43 | )), 44 | )); 45 | expect(find.text('foo bar'), findsOneWidget); 46 | await tester.tap(find.byType(OrgHeadlineWidget).first); 47 | await tester.pump(); 48 | expect(find.text('foo bar'), findsOneWidget); 49 | expect(find.text('baz buzz'), findsOneWidget); 50 | expect(find.textContaining('headline 2'), findsOneWidget); 51 | expect(find.text('bazinga'), findsNothing); 52 | await tester.restartAndRestore(); 53 | expect(find.text('foo bar'), findsOneWidget); 54 | expect(find.text('baz buzz'), findsOneWidget); 55 | expect(find.textContaining('headline 2'), findsOneWidget); 56 | expect(find.text('bazinga'), findsNothing); 57 | }); 58 | testWidgets('Ignores search on restore', (tester) async { 59 | final doc = OrgDocument.parse('''foo bar 60 | * headline 1 61 | baz buzz 62 | ** headline 2 63 | bazinga'''); 64 | final widget = OrgController( 65 | root: doc, 66 | searchQuery: RegExp('baz'), 67 | restorationId: 'doc', 68 | child: OrgRootWidget( 69 | child: OrgDocumentWidget(doc), 70 | ), 71 | ); 72 | await tester.pumpWidget(RootRestorationScope( 73 | restorationId: 'root', 74 | child: wrap(widget), 75 | )); 76 | expect(find.text('foo bar'), findsOneWidget); 77 | expect(find.textContaining('buzz'), findsOneWidget); 78 | expect(find.textContaining('headline 2'), findsOneWidget); 79 | expect(find.textContaining('inga'), findsOneWidget); 80 | await tester.tap(find.byType(OrgHeadlineWidget).last); 81 | await tester.pumpAndSettle(); 82 | expect(find.text('foo bar'), findsOneWidget); 83 | expect(find.textContaining('buzz'), findsOneWidget); 84 | expect(find.textContaining('headline 2'), findsOneWidget); 85 | expect(find.textContaining('inga'), findsNothing); 86 | await tester.restartAndRestore(); 87 | expect(find.text('foo bar'), findsOneWidget); 88 | expect(find.textContaining('buzz'), findsOneWidget); 89 | expect(find.textContaining('headline 2'), findsOneWidget); 90 | expect(find.textContaining('inga'), findsNothing); 91 | }); 92 | testWidgets('Ignores filter on restore', (tester) async { 93 | final doc = OrgDocument.parse('''foo bar 94 | * headline 1 95 | baz buzz 96 | ** headline 2 :abcd: 97 | bazinga'''); 98 | final widget = OrgController( 99 | root: doc, 100 | sparseQuery: const OrgQueryTagMatcher('abcd'), 101 | restorationId: 'doc', 102 | child: OrgRootWidget( 103 | child: OrgDocumentWidget(doc), 104 | ), 105 | ); 106 | await tester.pumpWidget(RootRestorationScope( 107 | restorationId: 'root', 108 | child: wrap(widget), 109 | )); 110 | expect(find.text('foo bar'), findsOneWidget); 111 | expect(find.text('baz buzz'), findsNothing); 112 | expect(find.textContaining('headline 2'), findsOneWidget); 113 | expect(find.text('bazinga'), findsNothing); 114 | await tester.tap(find.byType(OrgHeadlineWidget).last); 115 | await tester.pumpAndSettle(); 116 | expect(find.text('foo bar'), findsOneWidget); 117 | expect(find.text('baz buzz'), findsNothing); 118 | expect(find.textContaining('headline 2'), findsOneWidget); 119 | expect(find.text('bazinga'), findsOneWidget); 120 | await tester.restartAndRestore(); 121 | expect(find.text('foo bar'), findsOneWidget); 122 | expect(find.text('baz buzz'), findsNothing); 123 | expect(find.textContaining('headline 2'), findsOneWidget); 124 | expect(find.text('bazinga'), findsOneWidget); 125 | }); 126 | }); 127 | } 128 | -------------------------------------------------------------------------------- /test/widget/sub_superscript_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:org_flutter/org_flutter.dart'; 3 | 4 | import './util.dart'; 5 | 6 | void main() { 7 | testWidgets('Sub/superscript', (tester) async { 8 | await tester.pumpWidget(wrap(const Org(r'#+FOO: foo bar^{2} baz_{1}'))); 9 | expect(find.textContaining('2'), findsOneWidget); 10 | expect(find.textContaining('{2}'), findsNothing); 11 | expect(find.textContaining('1'), findsOneWidget); 12 | expect(find.textContaining('{1}'), findsNothing); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /test/widget/text_changes_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:org_flutter/org_flutter.dart'; 4 | 5 | import './util.dart'; 6 | 7 | class _TextProvider extends StatefulWidget { 8 | const _TextProvider({required this.text, required this.builder}); 9 | 10 | final Widget Function(String) builder; 11 | final String text; 12 | 13 | @override 14 | State<_TextProvider> createState() => _TextProviderState(); 15 | } 16 | 17 | class _TextProviderState extends State<_TextProvider> { 18 | late String _text; 19 | 20 | set text(String text) => setState(() => _text = text); 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | _text = widget.text; 26 | } 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return widget.builder(_text); 31 | } 32 | } 33 | 34 | void main() { 35 | group('Text changes', () { 36 | testWidgets('Org widget', (tester) async { 37 | final widget = _TextProvider( 38 | text: 'foo bar', 39 | builder: (text) => wrap(Org(text)), 40 | ); 41 | await tester.pumpWidget(widget); 42 | expect(find.text('foo bar'), findsOneWidget); 43 | final state = 44 | tester.state(find.byType(_TextProvider)) as _TextProviderState; 45 | state.text = 'baz buzz'; 46 | await tester.pump(); 47 | expect(find.text('foo bar'), findsNothing); 48 | expect(find.text('baz buzz'), findsOneWidget); 49 | }); 50 | testWidgets('OrgText widget', (tester) async { 51 | final widget = _TextProvider( 52 | text: 'foo bar', 53 | builder: (text) => wrap(OrgText(text)), 54 | ); 55 | await tester.pumpWidget(widget); 56 | expect(find.text('foo bar'), findsOneWidget); 57 | final state = 58 | tester.state(find.byType(_TextProvider)) as _TextProviderState; 59 | state.text = 'baz buzz'; 60 | await tester.pump(); 61 | expect(find.text('foo bar'), findsNothing); 62 | expect(find.text('baz buzz'), findsOneWidget); 63 | }); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /test/widget/util.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | Widget wrap(Widget child) { 4 | return Directionality( 5 | textDirection: TextDirection.ltr, 6 | child: Material(child: child), 7 | ); 8 | } 9 | --------------------------------------------------------------------------------