├── .github └── workflows │ └── dart.yml ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── example ├── example.dart └── pretty.dart ├── lib ├── org_parser.dart └── src │ ├── file_link │ ├── file_link.dart │ ├── grammar.dart │ ├── model.dart │ └── parser.dart │ ├── org │ ├── grammar.dart │ ├── model.dart │ ├── model │ │ ├── org_block.dart │ │ ├── org_citation.dart │ │ ├── org_comment.dart │ │ ├── org_content.dart │ │ ├── org_decrypted_content.dart │ │ ├── org_drawer.dart │ │ ├── org_dynamic_block.dart │ │ ├── org_entity.dart │ │ ├── org_fixed_width_area.dart │ │ ├── org_footnote.dart │ │ ├── org_headline.dart │ │ ├── org_horizontal_rule.dart │ │ ├── org_latex.dart │ │ ├── org_link.dart │ │ ├── org_link_target.dart │ │ ├── org_list.dart │ │ ├── org_local_variables.dart │ │ ├── org_macro_reference.dart │ │ ├── org_markup.dart │ │ ├── org_meta.dart │ │ ├── org_paragraph.dart │ │ ├── org_pgp_block.dart │ │ ├── org_plain_text.dart │ │ ├── org_planning_entry.dart │ │ ├── org_radio_target.dart │ │ ├── org_section.dart │ │ ├── org_statistics_cookie.dart │ │ ├── org_sub_superscript.dart │ │ ├── org_table.dart │ │ ├── org_timestamp.dart │ │ └── org_tree.dart │ ├── org.dart │ └── parser.dart │ ├── query │ ├── grammar.dart │ ├── model.dart │ ├── parser.dart │ └── query.dart │ ├── todo │ ├── grammar.dart │ ├── model.dart │ ├── parser.dart │ └── todo.dart │ ├── url.dart │ └── util │ ├── block.dart │ ├── bof.dart │ ├── drop.dart │ ├── elisp_regexp.dart │ ├── indent.dart │ ├── latex.dart │ ├── line.dart │ ├── line_breakable.dart │ ├── local_variables.dart │ ├── lookbehind.dart │ ├── noop.dart │ ├── unicode.dart │ ├── util.dart │ ├── whitespace.dart │ └── zipper.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── bin └── roundtrip.dart ├── edit_test.dart ├── example-gold.txt ├── file_link ├── grammar_test.dart ├── model_test.dart └── parser_test.dart ├── matchers.dart ├── org-manual.org ├── org-syntax.org ├── org ├── grammar │ ├── affiliated_keyword_test.dart │ ├── block_test.dart │ ├── citation_test.dart │ ├── comment_test.dart │ ├── content_test.dart │ ├── drawer_test.dart │ ├── dynamic_block_test.dart │ ├── entity_test.dart │ ├── fixed_width_area_test.dart │ ├── footnote_reference_test.dart │ ├── footnote_test.dart │ ├── grammar_test.dart │ ├── greater_block_test.dart │ ├── headline_test.dart │ ├── horizontal_rule_test.dart │ ├── inline_src_block_test.dart │ ├── link_target_test.dart │ ├── link_test.dart │ ├── list_test.dart │ ├── local_variables_test.dart │ ├── macro_reference_test.dart │ ├── markup_test.dart │ ├── paragraph_test.dart │ ├── pgp_block_test.dart │ ├── planning_entry.dart │ ├── property_test.dart │ ├── radio_link_test.dart │ ├── radio_target_test.dart │ ├── statistics_cookie_test.dart │ ├── subscript_test.dart │ ├── superscript_test.dart │ ├── table_test.dart │ └── timestamp_test.dart ├── model │ ├── arbitrary_greater_block_test.dart │ ├── block_test.dart │ ├── citation_test.dart │ ├── comment_test.dart │ ├── drawer_test.dart │ ├── dynamic_block_test.dart │ ├── entity_test.dart │ ├── fixed_width_area_test.dart │ ├── footnote_reference_test.dart │ ├── footnote_test.dart │ ├── greater_block_test.dart │ ├── header_test.dart │ ├── horizontal_rule_test.dart │ ├── inline_src_block_test.dart │ ├── latex_block_test.dart │ ├── latex_inline_test.dart │ ├── link_target_test.dart │ ├── link_test.dart │ ├── list_test.dart │ ├── local_variables_test.dart │ ├── macro_reference_test.dart │ ├── markups_test.dart │ ├── parser_test.dart │ ├── pgp_block_test.dart │ ├── planning_entry_test.dart │ ├── radio_link_test.dart │ ├── radio_target_test.dart │ ├── statistics_cookie_test.dart │ ├── subscript_test.dart │ ├── superscript_test.dart │ ├── table_test.dart │ └── timestamp_test.dart └── parser │ ├── block_test.dart │ ├── citation_test.dart │ ├── comment_test.dart │ ├── drawer_test.dart │ ├── dynamic_block_test.dart │ ├── entity_test.dart │ ├── fixed_width_area_test.dart │ ├── footnote_reference_test.dart │ ├── footnote_test.dart │ ├── greater_block_test.dart │ ├── headline_test.dart │ ├── horizontal_rule_test.dart │ ├── inline_src_block_test.dart │ ├── latex_block_test.dart │ ├── latex_inline_test.dart │ ├── link_target_test.dart │ ├── link_test.dart │ ├── list_test.dart │ ├── local_variables_test.dart │ ├── macro_reference_test.dart │ ├── markups_test.dart │ ├── parser_test.dart │ ├── pgp_block_test.dart │ ├── planning_entry_test.dart │ ├── radio_link_test.dart │ ├── radio_target_test.dart │ ├── statistics_cookie_test.dart │ ├── subscript_test.dart │ ├── superscript_test.dart │ ├── table_test.dart │ ├── timestamp_test.dart │ └── todo_states_test.dart ├── profile.dart ├── query ├── grammar_test.dart ├── matchers.dart ├── model_test.dart └── parser_test.dart ├── todo_test.dart ├── url_test.dart └── util_test.dart /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: dart-lang/setup-dart@v1 15 | - name: Install dependencies 16 | run: dart pub get 17 | - name: Analyze 18 | run: dart analyze 19 | - name: Run tests 20 | run: make test 21 | -------------------------------------------------------------------------------- /.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 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | 77 | /tmp 78 | *~ 79 | -------------------------------------------------------------------------------- /.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: fabeb2a16f1d008ab8230f450c49141d35669798 8 | channel: beta 9 | 10 | project_type: package 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 | SHELL := /usr/bin/env bash 2 | 3 | .PHONY: test 4 | test: ## Run all tests 5 | test: test-unit test-example test-roundtrip 6 | 7 | .PHONY: test-unit 8 | test-unit: 9 | dart test --chain-stack-traces 10 | 11 | .PHONY: test-example 12 | test-example: 13 | diff <(dart example/example.dart) test/example-gold.txt 14 | 15 | roundtrip = diff <(dart test/bin/roundtrip.dart $(1)) $(1) 16 | 17 | .PHONY: test-roundtrip 18 | test-roundtrip: 19 | $(call roundtrip,test/org-manual.org) 20 | $(call roundtrip,test/org-syntax.org) 21 | 22 | .PHONY: help 23 | help: ## Show this help text 24 | $(info usage: make [target]) 25 | $(info ) 26 | $(info Available targets:) 27 | @awk -F ':.*?## *' '/^[^\t].+?:.*?##/ \ 28 | {printf " %-24s %s\n", $$1, $$2}' $(MAKEFILE_LIST) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # org_parser 2 | 3 | An [Org Mode](https://orgmode.org/) parser for Dart. 4 | 5 | # Usage 6 | 7 | For displaying Org Mode documents in Flutter applications, see 8 | [org_flutter](https://github.com/amake/org_flutter). For an example application 9 | that displays Org Mode documents with org_parser and org_flutter, see 10 | [Orgro](https://orgro.org). 11 | 12 | This package allows you to parse raw Org Mode documents into a structured 13 | in-memory representation. 14 | 15 | ```dart 16 | import 'package:org_parser/org_parser.dart'; 17 | 18 | final doc = OrgDocument.parse('''* TODO [#A] foo bar 19 | baz buzz'''); 20 | print(doc.children[0].headline.keyword); // TODO 21 | ``` 22 | 23 | See the [example](./example/example.dart) for more. 24 | 25 | # Caveats 26 | 27 | This parser was developed for an application that is halfway between 28 | pretty-printing and evaluating/interpreting, so in many cases the parsed 29 | structure does not split out constituent parts as thoroughly as needed for some 30 | applications. 31 | 32 | # Supported syntax 33 | 34 | - Sections/headlines 35 | 36 | ```org 37 | * TODO [#A] foo bar 38 | ``` 39 | - Blocks 40 | 41 | ```org 42 | #+BEGIN_SRC 43 | foo bar 44 | #+END_SRC 45 | ``` 46 | - Inline src 47 | 48 | ```org 49 | foo src_sh{echo "bar"} baz 50 | ``` 51 | - Affiliated keywords 52 | 53 | ```org 54 | #+name: foo 55 | ``` 56 | - Fixed-width areas 57 | 58 | ```org 59 | : foo bar 60 | : baz buzz 61 | ``` 62 | - Tables 63 | 64 | ```org 65 | | foo | bar | 66 | |-----+-----| 67 | | biz | baz | 68 | ``` 69 | - Lists 70 | 71 | ```org 72 | - foo 73 | - [X] bar 74 | 1. baz 75 | 2. buzz 76 | ``` 77 | - Drawers 78 | 79 | ```org 80 | :PROPERTIES: 81 | foo bar 82 | :END: 83 | ``` 84 | - Footnotes 85 | 86 | ```org 87 | Foo bar[fn:1] biz buzz 88 | 89 | [fn:1] Bazinga 90 | ``` 91 | - Links 92 | 93 | ```org 94 | [[http://example.com][example]] 95 | 96 | http://example.com 97 | ``` 98 | - Emphasis markup 99 | 100 | ```org 101 | *bold* /italic/ _underline_ +strikethrough+ ~code~ =verbatim= 102 | ``` 103 | - Timestamps 104 | 105 | ```org 106 | [2020-05-05 Tue] 107 | 108 | <2020-05-05 Tue 10:00> 109 | ``` 110 | - Macro references 111 | 112 | ```org 113 | {{{kbd(C-c C-c)}}} 114 | ``` 115 | 116 | - LaTeX fragments 117 | 118 | ```org 119 | Then we add $a^2$ to \(b^2\) 120 | ``` 121 | 122 | ```org 123 | \begin{equation} 124 | \nabla \times \mathbf{B} = \frac{1}{c}\left( 4\pi\mathbf{J} + \frac{\partial \mathbf{E}}{\partial t}\right) 125 | \end{equation} 126 | ``` 127 | 128 | - Entities 129 | 130 | ```org 131 | a\leftrightarrow{}b conversion 132 | ``` 133 | 134 | - Citations 135 | 136 | ```org 137 | [cite:@key] 138 | ``` 139 | 140 | - Horizontal rules 141 | 142 | ```org 143 | ----- 144 | ``` 145 | - Radio targets 146 | 147 | ```org 148 | <<>> 149 | ``` 150 | 151 | - Link targets 152 | 153 | ```org 154 | <> 155 | ``` 156 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | language: 5 | strict-casts: true 6 | strict-raw-types: true 7 | strict-inference: true 8 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/org_parser.dart'; 2 | 3 | void main() { 4 | { 5 | // Parse a very simple document 6 | const docString = '''* TODO [#A] foo bar 7 | baz buzz'''; 8 | final doc = OrgDocument.parse(docString); 9 | final section = doc.sections[0]; 10 | print(section.headline.keyword?.value); 11 | final title = section.headline.title!.children[0] as OrgPlainText; 12 | print(title.content); 13 | final paragraph = section.content!.children[0] as OrgParagraph; 14 | final body = paragraph.body.children[0] as OrgPlainText; 15 | print(body.content); 16 | } 17 | 18 | { 19 | // Extract TODOs from a document 20 | const docString = '''* TODO Go fishing 21 | ** Equipment 22 | - Fishing rod 23 | - Bait 24 | - Hat 25 | * TODO Eat lunch 26 | ** Restaurants 27 | - Famous Ray's 28 | - Original Ray's 29 | * TODO Take a nap'''; 30 | final doc = OrgDocument.parse(docString); 31 | doc.visitSections((section) { 32 | if (section.headline.keyword?.value == 'TODO') { 33 | final title = section.headline.title!.children 34 | .whereType() 35 | .map((plainText) => plainText.content) 36 | .join(); 37 | print("I'm going to ${title.toLowerCase()}"); 38 | } 39 | return true; 40 | }); 41 | } 42 | 43 | { 44 | // Edit a document 45 | final docString = '''* TODO [#A] foo bar 46 | baz buzz'''; 47 | final doc = OrgDocument.parse(docString); 48 | final newDoc = doc 49 | .edit() 50 | .goDown() 51 | .goDown() 52 | .goRight() 53 | .goDown() 54 | .replace(OrgPlainText('bazinga')) 55 | .commit(); 56 | print(newDoc.toMarkup()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /example/pretty.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:org_parser/org_parser.dart'; 5 | 6 | void main(List arguments) async { 7 | final markup = arguments.isNotEmpty 8 | ? arguments.first 9 | : await stdin.transform(utf8.decoder).join(); 10 | final doc = OrgDocument.parse(markup); 11 | visit(doc); 12 | } 13 | 14 | void visit(OrgNode node, {int depth = 0}) { 15 | final preview = makePreview(node); 16 | print('${' ' * depth}$node: $preview'); 17 | if (node is OrgParentNode) { 18 | for (final child in node.children) { 19 | visit(child, depth: depth + 1); 20 | } 21 | } 22 | } 23 | 24 | String makePreview(OrgNode node) { 25 | final result = 26 | node.toMarkup(serializer: PreviewSerializer()).replaceAll('\n', r'\n'); 27 | if (result.trim().isEmpty) return '"$result"'; 28 | return result; 29 | } 30 | 31 | const previewLength = 10; 32 | 33 | class PreviewSerializer extends OrgSerializer { 34 | var _canceled = false; 35 | 36 | void cancel() => _canceled = true; 37 | 38 | @override 39 | void write(String text) { 40 | if (_canceled) return; 41 | super.write(text); 42 | if (length >= previewLength) cancel(); 43 | } 44 | 45 | @override 46 | void visit(OrgNode node) { 47 | if (_canceled) return; 48 | super.visit(node); 49 | } 50 | 51 | @override 52 | String toString() { 53 | final result = super.toString(); 54 | if (result.length <= previewLength) return result; 55 | return '${result.substring(0, previewLength)}...'; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/org_parser.dart: -------------------------------------------------------------------------------- 1 | export 'src/file_link/file_link.dart'; 2 | export 'src/org/org.dart'; 3 | export 'src/query/query.dart'; 4 | export 'src/todo/todo.dart'; 5 | export 'src/url.dart'; 6 | export 'src/util/zipper.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/file_link/file_link.dart: -------------------------------------------------------------------------------- 1 | export 'grammar.dart'; 2 | export 'model.dart'; 3 | export 'parser.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/file_link/grammar.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | /// Grammar rules for file links, which are basically a mini-format of their own 4 | class OrgFileLinkGrammarDefinition extends GrammarDefinition { 5 | @override 6 | Parser start() => 7 | ref0(scheme) & 8 | ref0(body) & 9 | (string('::') & ref0(extra)).pick(1).optional(); 10 | 11 | Parser scheme() => 12 | (string('file:') | string('attachment:') | anyOf('/.').and()) 13 | .flatten('Expected link scheme'); 14 | 15 | Parser body() => 16 | any().starLazy(string('::') | endOfInput()).flatten('Expected link body'); 17 | 18 | Parser extra() => any().starString('Expected link extra'); 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/file_link/model.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/file_link/parser.dart'; 2 | 3 | /// A link to a file, like 4 | /// ``` 5 | /// file:/foo/bar.org::#custom-id 6 | /// ``` 7 | class OrgFileLink { 8 | factory OrgFileLink.parse(String text) => 9 | orgFileLink.parse(text).value as OrgFileLink; 10 | 11 | OrgFileLink(this.scheme, this.body, this.extra); 12 | final String? scheme; 13 | final String body; 14 | final String? extra; 15 | 16 | /// Whether the file linked to is indicated by a relative path (as opposed to 17 | /// an absolute path). Also true for local links. 18 | bool get isRelative => 19 | isLocal || 20 | body.startsWith('.') || 21 | scheme != null && !body.startsWith('/'); 22 | 23 | /// Whether this link points to a section within the current document. 24 | bool get isLocal => body.isEmpty && extra != null; 25 | 26 | OrgFileLink copyWith({String? scheme, String? body, String? extra}) => 27 | OrgFileLink( 28 | scheme ?? this.scheme, 29 | body ?? this.body, 30 | extra ?? this.extra, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/file_link/parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/file_link/grammar.dart'; 2 | import 'package:org_parser/src/file_link/model.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | /// File link parser 6 | final orgFileLink = OrgFileLinkParserDefinition().build(); 7 | 8 | /// File link parser definition 9 | class OrgFileLinkParserDefinition extends OrgFileLinkGrammarDefinition { 10 | @override 11 | Parser start() => super.start().map((values) { 12 | final scheme = values[0] as String; 13 | final body = values[1] as String; 14 | final extra = values[2] as String?; 15 | return OrgFileLink( 16 | scheme.isEmpty ? null : scheme, 17 | body, 18 | extra, 19 | ); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/org/model/org_citation.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A citation like [cite:@key] 4 | class OrgCitation extends OrgLeafNode { 5 | String leading; 6 | ({String leading, String value})? style; 7 | String delimiter; 8 | String body; 9 | String trailing; 10 | 11 | OrgCitation( 12 | this.leading, 13 | this.style, 14 | this.delimiter, 15 | this.body, 16 | this.trailing, 17 | ); 18 | 19 | // TODO(aaron): This is dangerously close to needing its own parser 20 | List getKeys() => body 21 | .split(';') 22 | .expand((token) => token.split(' ')) 23 | .expand((token) => token.split(RegExp('(?=@)'))) 24 | .where((token) => token.startsWith('@')) 25 | .map((token) => token.substring(1)) 26 | .toList(growable: false); 27 | 28 | @override 29 | void _toMarkupImpl(OrgSerializer buf) { 30 | buf 31 | ..write(leading) 32 | ..write(style?.leading ?? '') 33 | ..write(style?.value ?? '') 34 | ..write(delimiter) 35 | ..write(body) 36 | ..write(trailing); 37 | } 38 | 39 | @override 40 | bool contains(Pattern pattern) { 41 | return leading.contains(pattern) || 42 | style?.value.contains(pattern) == true || 43 | delimiter.contains(pattern) || 44 | body.contains(pattern) || 45 | trailing.contains(pattern); 46 | } 47 | 48 | OrgCitation copyWith({ 49 | String? leading, 50 | ({String leading, String value})? style, 51 | String? delimiter, 52 | String? body, 53 | String? trailing, 54 | }) { 55 | return OrgCitation( 56 | leading ?? this.leading, 57 | style ?? this.style, 58 | delimiter ?? this.delimiter, 59 | body ?? this.body, 60 | trailing ?? this.trailing, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/org/model/org_comment.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | class OrgComment extends OrgLeafNode with OrgElement { 4 | OrgComment(this.indent, this.start, this.content, this.trailing); 5 | 6 | @override 7 | final String indent; 8 | final String start; 9 | final String content; 10 | @override 11 | final String trailing; 12 | 13 | @override 14 | bool contains(Pattern pattern) => 15 | indent.contains(pattern) || 16 | start.contains(pattern) || 17 | content.contains(pattern) || 18 | trailing.contains(pattern); 19 | 20 | @override 21 | _toMarkupImpl(OrgSerializer buf) { 22 | buf 23 | ..write(indent) 24 | ..write(start) 25 | ..write(content) 26 | ..write(trailing); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/org/model/org_content.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A generic node that contains children 4 | class OrgContent extends OrgParentNode { 5 | OrgContent(Iterable children, [super.id]) 6 | : children = children.length == 1 && children.firstOrNull is OrgContent 7 | ? (children.first as OrgContent).children 8 | : List.unmodifiable(children); 9 | 10 | @override 11 | final List children; 12 | 13 | @override 14 | OrgContent fromChildren(List children) => 15 | copyWith(children: children); 16 | 17 | @override 18 | bool contains(Pattern pattern) => 19 | children.any((child) => child.contains(pattern)); 20 | 21 | @override 22 | String toString() => 'OrgContent'; 23 | 24 | @override 25 | void _toMarkupImpl(OrgSerializer buf) { 26 | for (final child in children) { 27 | buf.visit(child); 28 | } 29 | } 30 | 31 | OrgContent copyWith({List? children, String? id}) => 32 | OrgContent(children ?? this.children, id ?? this.id); 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/org/model/org_decrypted_content.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | // This is an abstract class so that it can be sent to an isolate for processing 4 | abstract class DecryptedContentSerializer { 5 | String toMarkup(OrgDecryptedContent content); 6 | } 7 | 8 | class OrgDecryptedContent extends OrgTree { 9 | static OrgDecryptedContent fromDecryptedResult( 10 | String cleartext, 11 | DecryptedContentSerializer serializer, 12 | ) { 13 | final parsed = OrgDocument.parse(cleartext); 14 | return OrgDecryptedContent( 15 | serializer, 16 | parsed.content, 17 | parsed.sections, 18 | parsed.id, 19 | ); 20 | } 21 | 22 | OrgDecryptedContent( 23 | this.serializer, 24 | super.content, 25 | super.sections, 26 | super.id, 27 | ); 28 | 29 | final DecryptedContentSerializer serializer; 30 | 31 | @override 32 | void _toMarkupImpl(OrgSerializer buf) => buf.write(serializer.toMarkup(this)); 33 | 34 | String toCleartextMarkup({OrgSerializer? serializer}) { 35 | serializer ??= OrgSerializer(); 36 | for (final child in children) { 37 | serializer.visit(child); 38 | } 39 | return serializer.toString(); 40 | } 41 | 42 | @override 43 | String toString() => 'OrgDecryptedContent'; 44 | 45 | @override 46 | List get children => [if (content != null) content!, ...sections]; 47 | 48 | @override 49 | OrgParentNode fromChildren(List children) { 50 | final content = 51 | children.first is OrgContent ? children.first as OrgContent : null; 52 | final sections = content == null ? children : children.skip(1); 53 | return copyWith(content: content, sections: sections.cast()); 54 | } 55 | 56 | OrgDecryptedContent copyWith({ 57 | DecryptedContentSerializer? serializer, 58 | OrgContent? content, 59 | Iterable? sections, 60 | String? id, 61 | }) => 62 | OrgDecryptedContent( 63 | serializer ?? this.serializer, 64 | content ?? this.content, 65 | sections ?? this.sections, 66 | id ?? this.id, 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/org/model/org_drawer.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A drawer, like 4 | /// ``` 5 | /// :PROPERTIES: 6 | /// :CUSTOM_ID: foobar 7 | /// :END: 8 | /// ``` 9 | class OrgDrawer extends OrgParentNode with OrgElement { 10 | OrgDrawer( 11 | this.indent, 12 | this.header, 13 | this.body, 14 | this.footer, 15 | this.trailing, [ 16 | super.id, 17 | ]); 18 | 19 | @override 20 | final String indent; 21 | final String header; 22 | final OrgNode body; 23 | final String footer; 24 | @override 25 | final String trailing; 26 | 27 | @override 28 | List get children => [body]; 29 | 30 | @override 31 | OrgDrawer fromChildren(List children) => 32 | copyWith(body: children.single); 33 | 34 | /// Get a list of [OrgProperty] nodes contained within this block. Optionally 35 | /// filter the result to include only properties with the specified [key]. 36 | /// Keys are matched case-insensitively. 37 | List properties({String? key}) { 38 | final upperKey = key?.toUpperCase(); 39 | final result = []; 40 | visit((prop) { 41 | if (upperKey == null || prop.key.toUpperCase() == upperKey) { 42 | result.add(prop); 43 | } 44 | return true; 45 | }); 46 | return result; 47 | } 48 | 49 | @override 50 | bool contains(Pattern pattern) => 51 | header.contains(pattern) || 52 | body.contains(pattern) || 53 | footer.contains(pattern); 54 | 55 | @override 56 | String toString() => 'OrgDrawer'; 57 | 58 | @override 59 | void _toMarkupImpl(OrgSerializer buf) { 60 | buf 61 | ..write(indent) 62 | ..write(header) 63 | ..visit(body) 64 | ..write(footer) 65 | ..write(trailing); 66 | } 67 | 68 | OrgDrawer copyWith({ 69 | String? indent, 70 | String? header, 71 | OrgNode? body, 72 | String? footer, 73 | String? trailing, 74 | String? id, 75 | }) => 76 | OrgDrawer( 77 | indent ?? this.indent, 78 | header ?? this.header, 79 | body ?? this.body, 80 | footer ?? this.footer, 81 | trailing ?? this.trailing, 82 | id ?? this.id, 83 | ); 84 | } 85 | 86 | /// A property in a drawer, like 87 | /// ``` 88 | /// :CUSTOM_ID: foobar 89 | /// ``` 90 | class OrgProperty extends OrgParentNode with OrgElement { 91 | OrgProperty(this.indent, this.key, this.value, this.trailing, [super.id]); 92 | 93 | @override 94 | final String indent; 95 | final String key; 96 | final OrgContent value; 97 | @override 98 | final String trailing; 99 | 100 | @override 101 | bool contains(Pattern pattern) => 102 | key.contains(pattern) || value.contains(pattern); 103 | 104 | @override 105 | String toString() => 'OrgProperty'; 106 | 107 | @override 108 | void _toMarkupImpl(OrgSerializer buf) { 109 | buf 110 | ..write(indent) 111 | ..write(key) 112 | ..visit(value) 113 | ..write(trailing); 114 | } 115 | 116 | @override 117 | List get children => [value]; 118 | 119 | @override 120 | OrgParentNode fromChildren(List children) => 121 | copyWith(value: children.single as OrgContent); 122 | 123 | OrgProperty copyWith({ 124 | String? indent, 125 | String? key, 126 | OrgContent? value, 127 | String? trailing, 128 | String? id, 129 | }) => 130 | OrgProperty( 131 | indent ?? this.indent, 132 | key ?? this.key, 133 | value ?? this.value, 134 | trailing ?? this.trailing, 135 | id ?? this.id, 136 | ); 137 | } 138 | -------------------------------------------------------------------------------- /lib/src/org/model/org_dynamic_block.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// ``` 4 | /// #+BEGIN: myblockfunc :parameter1 value1 :parameter2 value2 5 | /// ... 6 | /// #+END: 7 | /// ``` 8 | class OrgDynamicBlock extends OrgParentNode with OrgElement { 9 | OrgDynamicBlock( 10 | this.indent, 11 | this.header, 12 | this.body, 13 | this.footer, 14 | this.trailing, [ 15 | super.id, 16 | ]); 17 | 18 | @override 19 | final String indent; 20 | final String header; 21 | final OrgContent body; 22 | final String footer; 23 | @override 24 | final String trailing; 25 | 26 | @override 27 | List get children => [body]; 28 | 29 | @override 30 | OrgDynamicBlock fromChildren(List children) => 31 | copyWith(body: children.single as OrgContent); 32 | 33 | @override 34 | bool contains(Pattern pattern) => 35 | header.contains(pattern) || 36 | body.contains(pattern) || 37 | footer.contains(pattern); 38 | 39 | @override 40 | String toString() => 'OrgDynamicBlock'; 41 | 42 | @override 43 | void _toMarkupImpl(OrgSerializer buf) { 44 | buf 45 | ..write(indent) 46 | ..write(header) 47 | ..visit(body) 48 | ..write(footer) 49 | ..write(trailing); 50 | } 51 | 52 | OrgDynamicBlock copyWith({ 53 | String? type, 54 | String? indent, 55 | String? header, 56 | OrgContent? body, 57 | String? footer, 58 | String? trailing, 59 | String? id, 60 | }) => 61 | OrgDynamicBlock( 62 | indent ?? this.indent, 63 | header ?? this.header, 64 | body ?? this.body, 65 | footer ?? this.footer, 66 | trailing ?? this.trailing, 67 | id ?? this.id, 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /lib/src/org/model/org_entity.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// An entity, like `\Omega` 4 | class OrgEntity extends OrgLeafNode { 5 | OrgEntity(this.leading, this.name, this.trailing); 6 | 7 | final String leading; 8 | final String name; 9 | final String trailing; 10 | 11 | @override 12 | bool contains(Pattern pattern) => 13 | leading.contains(pattern) || 14 | name.contains(pattern) || 15 | trailing.contains(pattern); 16 | 17 | @override 18 | _toMarkupImpl(OrgSerializer buf) { 19 | buf 20 | ..write(leading) 21 | ..write(name) 22 | ..write(trailing); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/org/model/org_fixed_width_area.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A fixed-width area, like 4 | /// ``` 5 | /// : result of source block, or whatever 6 | /// ``` 7 | class OrgFixedWidthArea extends OrgLeafNode with OrgElement { 8 | OrgFixedWidthArea(this.indent, this.content, this.trailing); 9 | 10 | @override 11 | final String indent; 12 | final String content; 13 | @override 14 | final String trailing; 15 | 16 | @override 17 | bool contains(Pattern pattern) => 18 | indent.contains(pattern) || 19 | content.contains(pattern) || 20 | trailing.contains(pattern); 21 | 22 | @override 23 | String toString() => 'OrgFixedWidthArea'; 24 | 25 | @override 26 | void _toMarkupImpl(OrgSerializer buf) { 27 | buf 28 | ..write(indent) 29 | ..write(content) 30 | ..write(trailing); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/org/model/org_horizontal_rule.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A horizontal rule, like 4 | /// ``` 5 | /// ----- 6 | /// ``` 7 | class OrgHorizontalRule extends OrgLeafNode with OrgElement { 8 | OrgHorizontalRule(this.indent, this.content, this.trailing); 9 | 10 | @override 11 | final String indent; 12 | final String content; 13 | @override 14 | final String trailing; 15 | 16 | @override 17 | String toString() => 'OrgMacroReference'; 18 | 19 | @override 20 | void _toMarkupImpl(OrgSerializer buf) { 21 | buf 22 | ..write(indent) 23 | ..write(content) 24 | ..write(trailing); 25 | } 26 | 27 | @override 28 | bool contains(Pattern pattern) => 29 | indent.contains(pattern) || 30 | content.contains(pattern) || 31 | trailing.contains(pattern); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/org/model/org_latex.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A LaTeX block, like 4 | /// ``` 5 | /// \begin{equation} 6 | /// \nabla \cdot \mathbf{B} = 0 7 | /// \end{equation} 8 | /// ``` 9 | class OrgLatexBlock extends OrgLeafNode with OrgElement { 10 | OrgLatexBlock( 11 | this.environment, 12 | this.indent, 13 | this.begin, 14 | this.content, 15 | this.end, 16 | this.trailing, 17 | ); 18 | 19 | /// The LaTeX environment, like `equation` 20 | final String environment; 21 | @override 22 | final String indent; 23 | final String begin; 24 | final String content; 25 | final String end; 26 | @override 27 | final String trailing; 28 | 29 | @override 30 | bool contains(Pattern pattern) => 31 | indent.contains(pattern) || 32 | begin.contains(pattern) || 33 | content.contains(pattern) || 34 | end.contains(pattern) || 35 | trailing.contains(pattern); 36 | 37 | @override 38 | String toString() => 'OrgLatexBlock'; 39 | 40 | @override 41 | _toMarkupImpl(OrgSerializer buf) { 42 | buf 43 | ..write(indent) 44 | ..write(begin) 45 | ..write(content) 46 | ..write(end) 47 | ..write(trailing); 48 | } 49 | } 50 | 51 | /// An inline LaTeX snippet, like `$E=mc^2$` 52 | class OrgLatexInline extends OrgLeafNode { 53 | OrgLatexInline( 54 | this.leadingDecoration, 55 | this.content, 56 | this.trailingDecoration, 57 | ); 58 | 59 | final String leadingDecoration; 60 | final String content; 61 | final String trailingDecoration; 62 | 63 | @override 64 | String toString() => 'OrgLatexInline'; 65 | 66 | @override 67 | bool contains(Pattern pattern) => 68 | leadingDecoration.contains(pattern) || 69 | content.contains(pattern) || 70 | trailingDecoration.contains(pattern); 71 | 72 | @override 73 | _toMarkupImpl(OrgSerializer buf) { 74 | buf 75 | ..write(leadingDecoration) 76 | ..write(content) 77 | ..write(trailingDecoration); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/org/model/org_link.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | mixin OrgLink on OrgNode { 4 | /// Where the link points 5 | String get location; 6 | } 7 | 8 | /// A link, like 9 | /// ``` 10 | /// https://example.com 11 | /// ``` 12 | class OrgPlainLink extends OrgLeafNode with OrgLink { 13 | OrgPlainLink(this.location); 14 | 15 | /// Where the link points 16 | @override 17 | final String location; 18 | 19 | @override 20 | bool contains(Pattern pattern) => location.contains(pattern); 21 | 22 | @override 23 | String toString() => 'OrgLink'; 24 | 25 | @override 26 | void _toMarkupImpl(OrgSerializer buf) { 27 | buf.write(location); 28 | } 29 | } 30 | 31 | /// A bracketed link, like 32 | /// ``` 33 | /// [[https://example.com][An example]] 34 | /// ``` 35 | /// or 36 | /// ``` 37 | /// [[https://example.com]] 38 | /// ``` 39 | class OrgBracketLink extends OrgParentNode with OrgLink { 40 | OrgBracketLink(this.location, this.description); 41 | 42 | /// Where the link points 43 | @override 44 | final String location; 45 | 46 | /// The user-visible text 47 | final OrgContent? description; 48 | 49 | @override 50 | bool contains(Pattern pattern) => 51 | location.contains(pattern) || description?.contains(pattern) == true; 52 | 53 | @override 54 | String toString() => 'OrgLink'; 55 | 56 | @override 57 | void _toMarkupImpl(OrgSerializer buf) { 58 | buf 59 | ..write('[[') 60 | ..write(location 61 | // Backslash must be first 62 | .replaceAll(r'\', r'\\') 63 | .replaceAll(r'[', r'\[') 64 | .replaceAll(r']', r'\]')); 65 | if (description != null) { 66 | buf 67 | ..write('][') 68 | ..write(description! 69 | .toMarkup() 70 | .replaceAll(']]', ']\u200b]') 71 | .replaceAll(RegExp(r']$'), ']\u200b')); 72 | } 73 | buf.write(']]'); 74 | } 75 | 76 | @override 77 | List get children => description == null ? const [] : [description!]; 78 | 79 | @override 80 | OrgParentNode fromChildren(List children) => 81 | copyWith(description: children.single as OrgContent); 82 | 83 | OrgBracketLink copyWith({ 84 | String? location, 85 | OrgContent? description, 86 | }) => 87 | OrgBracketLink( 88 | location ?? this.location, 89 | description ?? this.description, 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/org/model/org_link_target.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A link target, like 4 | /// ``` 5 | /// <> 6 | /// ``` 7 | class OrgLinkTarget extends OrgLeafNode { 8 | OrgLinkTarget(this.leading, this.body, this.trailing); 9 | 10 | final String leading; 11 | final String body; 12 | final String trailing; 13 | 14 | @override 15 | bool contains(Pattern pattern) => 16 | leading.contains(pattern) || 17 | body.contains(pattern) || 18 | trailing.contains(pattern); 19 | 20 | @override 21 | String toString() => 'OrgLinkTarget'; 22 | 23 | @override 24 | void _toMarkupImpl(OrgSerializer buf) { 25 | buf 26 | ..write(leading) 27 | ..write(body) 28 | ..write(trailing); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/org/model/org_local_variables.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | class OrgLocalVariables extends OrgLeafNode with OrgElement { 4 | OrgLocalVariables( 5 | this.start, 6 | Iterable<({String prefix, String content, String suffix})> content, 7 | this.end, 8 | this.trailing, 9 | ) : entries = List.unmodifiable(content); 10 | 11 | @override 12 | final String indent = ''; 13 | final String start; 14 | final List<({String prefix, String content, String suffix})> entries; 15 | final String end; 16 | @override 17 | final String trailing; 18 | 19 | String get contentString => entries.map((line) => line.content).join('\n'); 20 | 21 | @override 22 | bool contains(Pattern pattern) => 23 | start.contains(pattern) || 24 | entries.any((line) => line.content.contains(pattern)) || 25 | end.contains(pattern) || 26 | trailing.contains(pattern); 27 | 28 | @override 29 | _toMarkupImpl(OrgSerializer buf) { 30 | buf.write(start); 31 | for (final entry in entries) { 32 | buf 33 | ..write(entry.prefix) 34 | ..write(entry.content) 35 | ..write(entry.suffix); 36 | } 37 | buf 38 | ..write(end) 39 | ..write(trailing); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/org/model/org_macro_reference.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A macro reference, like 4 | /// ``` 5 | /// {{{my_macro}}} 6 | /// ``` 7 | class OrgMacroReference extends OrgLeafNode with SingleContentElement { 8 | OrgMacroReference(this.content); 9 | 10 | @override 11 | final String content; 12 | 13 | @override 14 | String toString() => 'OrgMacroReference'; 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/org/model/org_markup.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// Supported styles for [OrgMarkup] nodes 4 | enum OrgStyle { 5 | bold, 6 | verbatim, 7 | italic, 8 | strikeThrough, 9 | underline, 10 | code, 11 | } 12 | 13 | /// Emphasis markup, like 14 | /// ``` 15 | /// *bold* 16 | /// /italic/ 17 | /// +strikethrough+ 18 | /// ~code~ 19 | /// =verbatim= 20 | /// ``` 21 | /// 22 | /// See [OrgStyle] for supported emphasis types 23 | class OrgMarkup extends OrgParentNode { 24 | // TODO(aaron): Get rid of this hack 25 | OrgMarkup.just(String content, OrgStyle style) 26 | : this('', OrgContent([OrgPlainText(content)]), '', style); 27 | 28 | OrgMarkup( 29 | this.leadingDecoration, 30 | this.content, 31 | this.trailingDecoration, 32 | this.style, [ 33 | super.id, 34 | ]); 35 | 36 | final String leadingDecoration; 37 | final OrgContent content; 38 | final String trailingDecoration; 39 | final OrgStyle style; 40 | 41 | @override 42 | String toString() => 'OrgMarkup'; 43 | 44 | @override 45 | bool contains(Pattern pattern) => 46 | leadingDecoration.contains(pattern) || 47 | content.contains(pattern) || 48 | trailingDecoration.contains(pattern); 49 | 50 | @override 51 | void _toMarkupImpl(OrgSerializer buf) { 52 | buf 53 | ..write(leadingDecoration) 54 | ..visit(content) 55 | ..write(trailingDecoration); 56 | } 57 | 58 | @override 59 | List get children => [content]; 60 | 61 | @override 62 | OrgMarkup fromChildren(List children) => 63 | copyWith(content: children.single as OrgContent); 64 | 65 | OrgMarkup copyWith({ 66 | String? leadingDecoration, 67 | OrgContent? content, 68 | String? trailingDecoration, 69 | OrgStyle? style, 70 | String? id, 71 | }) => 72 | OrgMarkup( 73 | leadingDecoration ?? this.leadingDecoration, 74 | content ?? this.content, 75 | trailingDecoration ?? this.trailingDecoration, 76 | style ?? this.style, 77 | id ?? this.id, 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/org/model/org_meta.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A "meta" line, like 4 | /// ``` 5 | /// #+KEYWORD: some-named-thing 6 | /// ``` 7 | /// 8 | /// TODO(aaron): Should this be renamed to `OrgKeyword`? 9 | class OrgMeta extends OrgParentNode with OrgElement { 10 | OrgMeta(this.indent, this.key, this.value, this.trailing); 11 | 12 | @override 13 | final String indent; 14 | 15 | /// The key, including the leading `#+` and trailing `:`. 16 | final String key; 17 | final OrgContent? value; 18 | @override 19 | final String trailing; 20 | 21 | @override 22 | bool contains(Pattern pattern) => 23 | indent.contains(pattern) || 24 | key.contains(pattern) || 25 | value?.contains(pattern) == true || 26 | trailing.contains(pattern); 27 | 28 | @override 29 | String toString() => 'OrgMeta'; 30 | 31 | @override 32 | void _toMarkupImpl(OrgSerializer buf) { 33 | buf 34 | ..write(indent) 35 | ..write(key); 36 | if (value != null) { 37 | buf.visit(value!); 38 | } 39 | buf.write(trailing); 40 | } 41 | 42 | @override 43 | List get children => (value == null) ? const [] : [value!]; 44 | 45 | @override 46 | OrgParentNode fromChildren(List children) => 47 | copyWith(value: children.singleOrNull as OrgContent?); 48 | 49 | OrgMeta copyWith({ 50 | String? indent, 51 | String? key, 52 | OrgContent? value, 53 | String? trailing, 54 | }) { 55 | return OrgMeta( 56 | indent ?? this.indent, 57 | key ?? this.key, 58 | value ?? this.value, 59 | trailing ?? this.trailing, 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/org/model/org_paragraph.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | class OrgParagraph extends OrgParentNode with OrgElement { 4 | OrgParagraph(this.indent, this.body, this.trailing, [super.id]); 5 | 6 | @override 7 | final String indent; 8 | final OrgContent body; 9 | @override 10 | final String trailing; 11 | 12 | @override 13 | List get children => [body]; 14 | 15 | @override 16 | OrgParagraph fromChildren(List children) => 17 | copyWith(body: children.single as OrgContent); 18 | 19 | @override 20 | bool contains(Pattern pattern) => 21 | indent.contains(pattern) || 22 | body.contains(pattern) || 23 | trailing.contains(pattern); 24 | 25 | @override 26 | String toString() => 'OrgParagraph'; 27 | 28 | @override 29 | void _toMarkupImpl(OrgSerializer buf) { 30 | buf 31 | ..write(indent) 32 | ..visit(body) 33 | ..write(trailing); 34 | } 35 | 36 | OrgParagraph copyWith({ 37 | String? indent, 38 | OrgContent? body, 39 | String? trailing, 40 | String? id, 41 | }) => 42 | OrgParagraph( 43 | indent ?? this.indent, 44 | body ?? this.body, 45 | trailing ?? this.trailing, 46 | id ?? this.id, 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/org/model/org_pgp_block.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | class OrgPgpBlock extends OrgLeafNode with OrgElement { 4 | OrgPgpBlock(this.indent, this.header, this.body, this.footer, this.trailing); 5 | 6 | @override 7 | final String indent; 8 | final String header; 9 | final String body; 10 | final String footer; 11 | @override 12 | final String trailing; 13 | 14 | @override 15 | bool contains(Pattern pattern) => 16 | indent.contains(pattern) || 17 | header.contains(pattern) || 18 | body.contains(pattern) || 19 | footer.contains(pattern) || 20 | trailing.contains(pattern); 21 | 22 | @override 23 | _toMarkupImpl(OrgSerializer buf) { 24 | buf 25 | ..write(indent) 26 | ..write(header) 27 | ..write(body) 28 | ..write(footer) 29 | ..write(trailing); 30 | } 31 | 32 | /// Convert to RFC 4880 format for decryption 33 | /// 34 | /// See: https://www.rfc-editor.org/rfc/rfc4880#section-6.2 35 | // 36 | // Indent finagling consistent with `org-crypt--encrypted-text` 37 | String toRfc4880() => 38 | toMarkup().trim().replaceAll(RegExp('^[ \t]*', multiLine: true), ''); 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/org/model/org_plain_text.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// Plain text that has no markup 4 | class OrgPlainText extends OrgLeafNode with SingleContentElement { 5 | OrgPlainText(this.content); 6 | 7 | @override 8 | final String content; 9 | 10 | @override 11 | String toString() => 'OrgPlainText'; 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/org/model/org_planning_entry.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// A planning keyword, like `SCHEDULED:` or `DEADLINE:` 4 | class OrgPlanningKeyword extends OrgLeafNode with SingleContentElement { 5 | OrgPlanningKeyword(this.content); 6 | 7 | @override 8 | final String content; 9 | 10 | @override 11 | String toString() => 'OrgKeyword'; 12 | } 13 | 14 | /// A planning line, like 15 | /// ``` 16 | /// SCHEDULED: <2021-12-09 Thu> 17 | /// ``` 18 | /// or 19 | /// ``` 20 | /// CLOSED: [2021-12-09 Thu 12:02] 21 | /// ``` 22 | class OrgPlanningEntry extends OrgParentNode { 23 | OrgPlanningEntry( 24 | this.keyword, 25 | this.separator, 26 | this.value, [ 27 | super.id, 28 | ]); 29 | 30 | final OrgPlanningKeyword keyword; 31 | final String separator; 32 | final OrgNode value; 33 | 34 | @override 35 | List get children => [keyword, value]; 36 | 37 | @override 38 | OrgPlanningEntry fromChildren(List children) => copyWith( 39 | keyword: children[0] as OrgPlanningKeyword, 40 | value: children[1], 41 | ); 42 | 43 | @override 44 | bool contains(Pattern pattern) => 45 | keyword.contains(pattern) || 46 | separator.contains(pattern) || 47 | value.contains(pattern); 48 | 49 | @override 50 | String toString() => 'OrgPlanningEntry'; 51 | 52 | @override 53 | void _toMarkupImpl(OrgSerializer buf) { 54 | buf 55 | ..visit(keyword) 56 | ..write(separator) 57 | ..visit(value); 58 | } 59 | 60 | OrgPlanningEntry copyWith({ 61 | OrgPlanningKeyword? keyword, 62 | String? separator, 63 | OrgNode? value, 64 | String? id, 65 | }) => 66 | OrgPlanningEntry( 67 | keyword ?? this.keyword, 68 | separator ?? this.separator, 69 | value ?? this.value, 70 | id ?? this.id, 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/org/model/org_radio_target.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// Extracts the radio targets from the given [tree]. 4 | Set extractRadioTargets( 5 | OrgTree tree, 6 | ) { 7 | final results = {}; 8 | tree.visit((target) { 9 | final t = target.body.toLowerCase().trim(); 10 | if (t.isNotEmpty) { 11 | results.add(t); 12 | } 13 | return true; 14 | }); 15 | return results; 16 | } 17 | 18 | /// A link target, like 19 | /// ``` 20 | /// <<>> 21 | /// ``` 22 | class OrgRadioTarget extends OrgLeafNode { 23 | OrgRadioTarget(this.leading, this.body, this.trailing); 24 | 25 | final String leading; 26 | final String body; 27 | final String trailing; 28 | 29 | @override 30 | bool contains(Pattern pattern) => 31 | leading.contains(pattern) || 32 | body.contains(pattern) || 33 | trailing.contains(pattern); 34 | 35 | @override 36 | String toString() => 'OrgRadioTarget'; 37 | 38 | @override 39 | void _toMarkupImpl(OrgSerializer buf) { 40 | buf 41 | ..write(leading) 42 | ..write(body) 43 | ..write(trailing); 44 | } 45 | } 46 | 47 | /// A word linkified to point to a radio target. This can only appear in a 48 | /// parsed tree if radio targets were supplied to the parser. 49 | class OrgRadioLink extends OrgLeafNode with SingleContentElement { 50 | OrgRadioLink(this.content); 51 | 52 | /// Where the link points 53 | @override 54 | final String content; 55 | 56 | @override 57 | String toString() => 'OrgRadioLink'; 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/org/model/org_section.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// An Org section. May have nested sections, like 4 | /// 5 | /// ``` 6 | /// * TODO [#A] COMMENT Title :tag1:tag2: 7 | /// content 8 | /// ** Sub section 9 | /// more content 10 | /// ``` 11 | class OrgSection extends OrgTree { 12 | OrgSection( 13 | this.headline, 14 | super.content, [ 15 | super.sections, 16 | super.id, 17 | ]); 18 | final OrgHeadline headline; 19 | 20 | /// The section's tags. Convenience accessor for tags of [headline]. 21 | List get tags => headline.tags?.values ?? const []; 22 | 23 | /// Returns the tags of this section and all parent sections. 24 | List tagsWithInheritance(OrgTree doc) => 25 | doc 26 | .find((node) => identical(node, this)) 27 | ?.path 28 | .whereType() 29 | .fold>([], (acc, node) => acc..addAll(node.tags)) ?? 30 | const []; 31 | 32 | @override 33 | List get children => [headline, ...super.children]; 34 | 35 | @override 36 | OrgSection fromChildren(List children) { 37 | final headline = children.first as OrgHeadline; 38 | if (children.length < 2) { 39 | return copyWith(headline: headline); 40 | } 41 | final content = 42 | children[1] is OrgContent ? children[1] as OrgContent : null; 43 | final sections = content == null ? children.skip(1) : children.skip(2); 44 | return copyWith( 45 | headline: headline, 46 | content: content, 47 | sections: sections.cast(), 48 | ); 49 | } 50 | 51 | int get level => headline.level; 52 | 53 | /// A section may be empty if it has no content or sub-sections 54 | bool get isEmpty => content == null && sections.isEmpty; 55 | 56 | OrgSection copyWith({ 57 | OrgHeadline? headline, 58 | OrgContent? content, 59 | Iterable? sections, 60 | String? id, 61 | }) => 62 | OrgSection( 63 | headline ?? this.headline, 64 | content ?? this.content, 65 | sections ?? this.sections, 66 | id ?? this.id, 67 | ); 68 | 69 | @override 70 | bool contains(Pattern pattern, {bool includeChildren = true}) => 71 | headline.contains(pattern) || 72 | super.contains(pattern, includeChildren: includeChildren); 73 | 74 | @override 75 | String toString() => 'OrgSection'; 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/org/model/org_statistics_cookie.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | sealed class OrgStatisticsCookie extends OrgLeafNode { 4 | OrgStatisticsCookie(this.leading, this.trailing); 5 | 6 | final String leading; 7 | final String trailing; 8 | 9 | bool get done; 10 | 11 | OrgStatisticsCookie update({required int done, required int total}); 12 | } 13 | 14 | class OrgStatisticsFractionCookie extends OrgStatisticsCookie { 15 | OrgStatisticsFractionCookie( 16 | super.leading, 17 | this.numerator, 18 | this.separator, 19 | this.denominator, 20 | super.trailing, 21 | ); 22 | 23 | final String numerator; 24 | final String separator; 25 | final String denominator; 26 | 27 | @override 28 | bool get done => numerator.isNotEmpty && numerator == denominator; 29 | 30 | @override 31 | OrgStatisticsCookie update({required int done, required int total}) => 32 | copyWith(numerator: done.toString(), denominator: total.toString()); 33 | 34 | @override 35 | String toString() => 'OrgStatisticsFractionCookie'; 36 | 37 | @override 38 | void _toMarkupImpl(OrgSerializer buf) { 39 | buf 40 | ..write(leading) 41 | ..write(numerator) 42 | ..write(separator) 43 | ..write(denominator) 44 | ..write(trailing); 45 | } 46 | 47 | @override 48 | bool contains(Pattern pattern) => 49 | leading.contains(pattern) || 50 | numerator.contains(pattern) || 51 | separator.contains(pattern) || 52 | denominator.contains(pattern) || 53 | trailing.contains(pattern); 54 | 55 | OrgStatisticsFractionCookie copyWith({ 56 | String? leading, 57 | String? numerator, 58 | String? separator, 59 | String? denominator, 60 | String? trailing, 61 | }) => 62 | OrgStatisticsFractionCookie( 63 | leading ?? this.leading, 64 | numerator ?? this.numerator, 65 | separator ?? this.separator, 66 | denominator ?? this.denominator, 67 | trailing ?? this.trailing, 68 | ); 69 | } 70 | 71 | class OrgStatisticsPercentageCookie extends OrgStatisticsCookie { 72 | OrgStatisticsPercentageCookie( 73 | super.leading, 74 | this.percentage, 75 | this.suffix, 76 | super.trailing, 77 | ); 78 | 79 | final String percentage; 80 | final String suffix; 81 | 82 | @override 83 | bool get done => percentage == '100'; 84 | 85 | @override 86 | OrgStatisticsCookie update({required int done, required int total}) => 87 | copyWith( 88 | percentage: done == 0 ? '0' : (done / total * 100).round().toString(), 89 | ); 90 | 91 | @override 92 | String toString() => 'OrgStatisticsPercentageCookie'; 93 | 94 | @override 95 | void _toMarkupImpl(OrgSerializer buf) { 96 | buf 97 | ..write(leading) 98 | ..write(percentage) 99 | ..write(suffix) 100 | ..write(trailing); 101 | } 102 | 103 | @override 104 | bool contains(Pattern pattern) => 105 | leading.contains(pattern) || 106 | percentage.contains(pattern) || 107 | suffix.contains(pattern) || 108 | trailing.contains(pattern); 109 | 110 | OrgStatisticsPercentageCookie copyWith({ 111 | String? leading, 112 | String? percentage, 113 | String? suffix, 114 | String? trailing, 115 | }) => 116 | OrgStatisticsPercentageCookie( 117 | leading ?? this.leading, 118 | percentage ?? this.percentage, 119 | suffix ?? this.suffix, 120 | trailing ?? this.trailing, 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/org/model/org_sub_superscript.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | sealed class OrgSubSuperscript extends OrgParentNode { 4 | OrgSubSuperscript(this.leading, this.body, this.trailing); 5 | 6 | final String leading; 7 | final OrgContent body; 8 | final String trailing; 9 | 10 | @override 11 | List get children => [body]; 12 | 13 | @override 14 | bool contains(Pattern pattern) => 15 | leading.contains(pattern) || 16 | body.contains(pattern) || 17 | trailing.contains(pattern); 18 | 19 | @override 20 | _toMarkupImpl(OrgSerializer buf) { 21 | buf 22 | ..write(leading) 23 | ..visit(body) 24 | ..write(trailing); 25 | } 26 | } 27 | 28 | class OrgSubscript extends OrgSubSuperscript { 29 | OrgSubscript(super.leading, super.body, super.trailing); 30 | 31 | @override 32 | String toString() => 'OrgSubscript'; 33 | 34 | @override 35 | OrgSubscript fromChildren(List children) => 36 | copyWith(body: children.single as OrgContent); 37 | 38 | OrgSubscript copyWith({ 39 | String? leading, 40 | OrgContent? body, 41 | String? trailing, 42 | }) => 43 | OrgSubscript( 44 | leading ?? this.leading, 45 | body ?? this.body, 46 | trailing ?? this.trailing, 47 | ); 48 | } 49 | 50 | class OrgSuperscript extends OrgSubSuperscript { 51 | OrgSuperscript(super.leading, super.body, super.trailing); 52 | 53 | @override 54 | String toString() => 'OrgSuperscript'; 55 | 56 | @override 57 | OrgSuperscript fromChildren(List children) => 58 | copyWith(body: children.single as OrgContent); 59 | 60 | OrgSuperscript copyWith({ 61 | String? leading, 62 | OrgContent? body, 63 | String? trailing, 64 | }) => 65 | OrgSuperscript( 66 | leading ?? this.leading, 67 | body ?? this.body, 68 | trailing ?? this.trailing, 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/org/model/org_tree.dart: -------------------------------------------------------------------------------- 1 | part of '../model.dart'; 2 | 3 | /// The top-level node representing a full Org document 4 | class OrgDocument extends OrgTree { 5 | /// Parse an Org document in string form into an AST. If 6 | /// [interpretEmbeddedSettings] is true, the document may be parsed a second 7 | /// time in order to apply detected settings. 8 | factory OrgDocument.parse( 9 | String text, { 10 | bool interpretEmbeddedSettings = false, 11 | }) { 12 | var parsed = org.parse(text).value as OrgDocument; 13 | 14 | if (interpretEmbeddedSettings) { 15 | final todoSettings = extractTodoSettings(parsed); 16 | final haveTodoSettings = todoSettings.any((s) => s != defaultTodoStates); 17 | final radioTargets = extractRadioTargets(parsed); 18 | final haveRadioTargets = radioTargets.any((t) => t.isNotEmpty); 19 | if (haveTodoSettings || haveRadioTargets) { 20 | final parser = OrgParserDefinition( 21 | todoStates: haveTodoSettings ? todoSettings : null, 22 | radioTargets: 23 | haveRadioTargets ? radioTargets.toList(growable: false) : null, 24 | ).build(); 25 | parsed = parser.parse(text).value as OrgDocument; 26 | } 27 | } 28 | 29 | return parsed; 30 | } 31 | 32 | OrgDocument(super.content, super.sections, [super.id]); 33 | 34 | @override 35 | String toString() => 'OrgDocument'; 36 | 37 | OrgDocument copyWith({ 38 | OrgContent? content, 39 | Iterable? sections, 40 | String? id, 41 | }) => 42 | OrgDocument( 43 | content ?? this.content, 44 | sections ?? this.sections, 45 | id ?? this.id, 46 | ); 47 | 48 | @override 49 | OrgDocument fromChildren(List children) { 50 | if (children.isEmpty) { 51 | return copyWith(content: null, sections: []); 52 | } 53 | final content = 54 | children.first is OrgContent ? children.first as OrgContent : null; 55 | final sections = content == null ? children : children.skip(1); 56 | return copyWith(content: content, sections: sections.cast()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/org/org.dart: -------------------------------------------------------------------------------- 1 | export 'grammar.dart'; 2 | export 'model.dart'; 3 | export 'parser.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/query/grammar.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/util/util.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | /// Grammar rules for the section query language described at 5 | /// https://orgmode.org/manual/Matching-tags-and-properties.html 6 | class OrgQueryGrammarDefinition extends GrammarDefinition { 7 | @override 8 | Parser start() => ref0(alternates).end(); 9 | 10 | Parser alternates() => ref0(alternate) | ref0(selection); 11 | 12 | Parser alternate() => ref0(selection) & char('|') & ref0(alternates); 13 | 14 | Parser selection() => 15 | ref0(explicitAnd) | ref0(implicitAnd) | ref0(simpleSelection); 16 | 17 | Parser explicitAnd() => ref0(simpleSelection) & char('&') & ref0(selection); 18 | 19 | Parser implicitAnd() => ref0(simpleSelection) & ref0(continuingSelection); 20 | 21 | Parser continuingSelection() => 22 | ref0(continuingExplicitAnd) | 23 | ref0(continuingImplicitAnd) | 24 | ref0(continuingSimpleSelection); 25 | 26 | Parser continuingExplicitAnd() => 27 | ref0(continuingSimpleSelection) & char('&') & ref0(selection); 28 | 29 | Parser continuingImplicitAnd() => 30 | ref0(continuingSimpleSelection) & ref0(continuingSelection); 31 | 32 | Parser simpleSelection() => ref0(exclude) | ref0(include); 33 | 34 | Parser continuingSimpleSelection() => ref0(explicitInclude) | ref0(exclude); 35 | 36 | Parser include() => ref0(explicitInclude) | ref0(implicitInclude); 37 | 38 | Parser explicitInclude() => char('+') & ref0(element); 39 | 40 | Parser implicitInclude() => ref0(element); 41 | 42 | Parser exclude() => char('-') & ref0(element); 43 | 44 | // TODO(aaron): Support regex element: {^boss.*} 45 | Parser element() => ref0(propertyExpression) | ref0(tag); 46 | 47 | Parser tag() => (alnum() | anyOf('_@#%')).plus().flatten('Tag expected'); 48 | 49 | Parser propertyExpression() => 50 | ref0(propertyName) & ref0(op) & ref0(propertyValue); 51 | 52 | Parser propertyName() => insignificantWhitespace() 53 | .neg() 54 | .plusLazy(ref0(op)) 55 | .flatten('Property name expected'); 56 | 57 | Parser op() => 58 | string('<=') | 59 | string('=>') | 60 | string('>=') | 61 | string('=<') | 62 | string('<>') | 63 | string('!=') | 64 | string('==') | 65 | anyOf('<=>'); 66 | 67 | // TODO(aaron): Support regex values (With={Sarah\|Denny}), dates 68 | // (SCHEDULED>="<2008-10-11>") 69 | Parser propertyValue() => ref0(numberValue) | ref0(stringValue); 70 | 71 | Parser numberValue() => digit().plusString(); 72 | 73 | Parser stringValue() => 74 | char('"') & 75 | char('"').neg().plusLazy(char('"')).flatten('String content expected') & 76 | char('"'); 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/query/parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/query/query.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | final orgQuery = OrgQueryParserDefinition().build(); 5 | 6 | class OrgQueryParserDefinition extends OrgQueryGrammarDefinition { 7 | @override 8 | Parser alternate() => super.alternate().map((value) => OrgQueryOrMatcher([ 9 | value[0] as OrgQueryMatcher, 10 | value[2] as OrgQueryMatcher, 11 | ])); 12 | 13 | @override 14 | Parser explicitAnd() => 15 | super.explicitAnd().map((value) => OrgQueryAndMatcher([ 16 | value[0] as OrgQueryMatcher, 17 | value[2] as OrgQueryMatcher, 18 | ])); 19 | 20 | @override 21 | Parser implicitAnd() => super 22 | .implicitAnd() 23 | .castList() 24 | .map((value) => OrgQueryAndMatcher(value)); 25 | 26 | @override 27 | Parser continuingExplicitAnd() => 28 | super.continuingExplicitAnd().map((value) => OrgQueryAndMatcher([ 29 | value[0] as OrgQueryMatcher, 30 | value[2] as OrgQueryMatcher, 31 | ])); 32 | 33 | @override 34 | Parser continuingImplicitAnd() => super 35 | .continuingImplicitAnd() 36 | .castList() 37 | .map((value) => OrgQueryAndMatcher(value)); 38 | 39 | @override 40 | Parser explicitInclude() => super.explicitInclude().map((value) => value[1]); 41 | 42 | @override 43 | Parser exclude() => super 44 | .exclude() 45 | .map((value) => OrgQueryNotMatcher(value[1] as OrgQueryMatcher)); 46 | 47 | @override 48 | Parser tag() => 49 | super.tag().cast().map((value) => OrgQueryTagMatcher(value)); 50 | 51 | @override 52 | Parser propertyExpression() => 53 | super.propertyExpression().map((value) => OrgQueryPropertyMatcher( 54 | property: value[0] as String, 55 | operator: value[1] as String, 56 | value: value[2], 57 | )); 58 | 59 | @override 60 | Parser numberValue() => 61 | super.numberValue().cast().map((value) => num.parse(value)); 62 | 63 | @override 64 | Parser stringValue() => super.stringValue().map((value) => value[1]); 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/query/query.dart: -------------------------------------------------------------------------------- 1 | export 'grammar.dart'; 2 | export 'model.dart'; 3 | export 'parser.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/todo/grammar.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | class TodoGrammar extends GrammarDefinition { 4 | @override 5 | Parser start() => ref0(workflow).end(); 6 | 7 | Parser workflow() => 8 | ref0(todoStates).optional() & 9 | ref0(doneStates).optional().map((items) => items?[1]); 10 | 11 | Parser todoStates() => 12 | ref0(todoState).trim().plusLazy(char('|') | endOfInput()); 13 | 14 | Parser doneStates() => char('|') & ref0(todoStates); 15 | 16 | Parser todoState() => 17 | // FIXME(aaron): This is kind of insane. Actual Org Mode is very 18 | // imperative: it splits by whitespace and then parses out the annotation 19 | // with regex 20 | whitespace() 21 | .neg() 22 | .plusLazy(ref0(annotation) | whitespace() | endOfInput()) 23 | .flatten('state name expected') 24 | .where((result) => result != '|') & 25 | ref0(annotation).optional(); 26 | 27 | Parser annotation() => 28 | char('(') & 29 | char(')').neg().plusGreedy(char(')')).flatten('annotation expected') & 30 | char(')'); 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/todo/model.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/org_parser.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | const defaultTodoStates = OrgTodoStates._(todo: ['TODO'], done: ['DONE']); 5 | 6 | const _todoMetaKeywords = ['#+TODO:', '#+SEQ_TODO:', '#+TYP_TODO:']; 7 | 8 | /// Extracts the TODO settings from `#+TODO:` and equivalent meta keywords in 9 | /// the given [tree]. 10 | List extractTodoSettings( 11 | OrgTree tree, 12 | ) { 13 | final results = []; 14 | tree.visit((meta) { 15 | if (_todoMetaKeywords.contains(meta.key.toUpperCase())) { 16 | final value = meta.value?.toMarkup().trim() ?? ''; 17 | final parsed = orgTodo.parse(value); 18 | if (parsed is Failure) { 19 | return true; 20 | } 21 | results.add(parsed.value as OrgTodoStates); 22 | } 23 | return true; 24 | }); 25 | return results; 26 | } 27 | 28 | /// A class representing the TODO states a la `org-todo-keywords`. Extract such 29 | /// states embedded in a document using [extractTodoSettings]. 30 | class OrgTodoStates { 31 | final List todo; 32 | final List done; 33 | 34 | OrgTodoStates({Iterable? todo, Iterable? done}) 35 | : todo = List.unmodifiable(todo ?? []), 36 | done = List.unmodifiable(done ?? []); 37 | 38 | const OrgTodoStates._({required this.todo, required this.done}); 39 | 40 | bool get isEmpty => todo.isEmpty && done.isEmpty; 41 | bool get isNotEmpty => !isEmpty; 42 | 43 | @override 44 | String toString() => 'TodoStates[${todo.join(' ')} | ${done.join(' ')}]'; 45 | 46 | @override 47 | bool operator ==(Object other) => 48 | other is OrgTodoStates && 49 | _listEquals(todo, other.todo) && 50 | _listEquals(done, other.done); 51 | 52 | @override 53 | int get hashCode => Object.hash(Object.hashAll(todo), Object.hashAll(done)); 54 | } 55 | 56 | bool _listEquals(List? a, List? b) { 57 | if (a == null) { 58 | return b == null; 59 | } 60 | if (b == null || a.length != b.length) { 61 | return false; 62 | } 63 | if (identical(a, b)) { 64 | return true; 65 | } 66 | for (int index = 0; index < a.length; index += 1) { 67 | if (a[index] != b[index]) { 68 | return false; 69 | } 70 | } 71 | return true; 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/todo/parser.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/todo/grammar.dart'; 2 | import 'package:org_parser/src/todo/model.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | final orgTodo = TodoParser().build(); 6 | 7 | class TodoParser extends TodoGrammar { 8 | @override 9 | Parser workflow() => super.workflow().castList?>().map((items) { 10 | // Discard annotations for now 11 | final todo = items[0] 12 | ?.map((state) => state[0] as String) 13 | .toList(growable: false); 14 | final done = items[1] 15 | ?.map((state) => state[0] as String) 16 | .toList(growable: false); 17 | 18 | // Last todo state is considered done if no done states are provided 19 | if (done == null && todo != null && todo.isNotEmpty) { 20 | return OrgTodoStates( 21 | todo: todo.getRange(0, todo.length - 1), 22 | done: [todo.last], 23 | ); 24 | } 25 | return OrgTodoStates(todo: todo, done: done); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/todo/todo.dart: -------------------------------------------------------------------------------- 1 | export 'grammar.dart'; 2 | export 'model.dart'; 3 | export 'parser.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/url.dart: -------------------------------------------------------------------------------- 1 | /// Identify URLs that point to a section within the current document (starting 2 | /// with '*') 3 | bool isOrgLocalSectionUrl(String url) => url.startsWith('*'); 4 | 5 | /// Return the title of the section pointed to by the URL. The URL must be one 6 | /// for which [isOrgLocalSectionUrl] returns true. 7 | String parseOrgLocalSectionUrl(String url) { 8 | assert(isOrgLocalSectionUrl(url)); 9 | return url.substring(1).replaceAll(RegExp('[ \t]*\r?\n[ \t]*'), ' '); 10 | } 11 | 12 | /// Identify URLs that point to a custom ID (starting with '#'). 13 | /// 14 | /// Note that "custom IDs" are distinct from "IDs"; see [isOrgIdUrl]. 15 | bool isOrgCustomIdUrl(String url) => url.startsWith('#'); 16 | 17 | /// Return the CUSTOM_ID of the section pointed to by the URL. The URL must be 18 | /// one for which [isOrgCustomIdUrl] returns true. 19 | String parseOrgCustomIdUrl(String url) { 20 | assert(isOrgCustomIdUrl(url)); 21 | return url.substring(1); 22 | } 23 | 24 | /// Identify URLs that point to IDs (starting with 'id:'). 25 | /// 26 | /// Note that "IDs" are distinct from "custom IDs"; see [isOrgCustomIdUrl]. 27 | bool isOrgIdUrl(String url) => url.startsWith('id:'); 28 | 29 | /// Return the ID of the section pointed to by the URL. The URL must be one 30 | /// for which [isOrgCustomIdUrl] returns true. 31 | String parseOrgIdUrl(String url) { 32 | assert(isOrgIdUrl(url)); 33 | return url.substring(3); 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/util/block.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/util/util.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | final _start = stringIgnoreCase('#+begin_') & 5 | whitespace().neg().plusString('Block name expected') & 6 | lineTrailing().flatten('Trailing line content expected'); 7 | 8 | Parser blockParser([Parser? delegate]) => 9 | BlockParser(delegate ?? any().starString('Block content expected')); 10 | 11 | class BlockParser extends DelegateParser> { 12 | BlockParser(super.delegate); 13 | 14 | @override 15 | BlockParser copy() => BlockParser(delegate); 16 | 17 | @override 18 | Result> parseOn(Context context) { 19 | final startResult = _start.parseOn(context); 20 | if (startResult is Failure) { 21 | return startResult; 22 | } 23 | final name = startResult.value[1]; 24 | final endPattern = RegExp( 25 | '^(\\s*)(${RegExp.escape('#+end_$name')})', 26 | caseSensitive: false, 27 | multiLine: true, 28 | ); 29 | final endMatch = 30 | endPattern.allMatches(context.buffer, startResult.position).firstOrNull; 31 | if (endMatch == null) { 32 | return context.failure('Block end expected'); 33 | } 34 | final contentResult = delegate.parseOn(Context( 35 | context.buffer.substring(startResult.position, endMatch.start), 0)); 36 | if (contentResult is Failure) { 37 | return contentResult; 38 | } 39 | return context.success( 40 | [ 41 | // Returning the name as if it was a part of the content is a hack. I 42 | // would have preferred to return a record like 43 | // 44 | // (type: name, parts: [...]) 45 | // 46 | // but records containing lists would need a custom matcher, and I just 47 | // can't be bothered with all that. 48 | name, 49 | startResult.value, 50 | contentResult.value, 51 | [endMatch[1]!, endMatch[2]!], 52 | ], 53 | endMatch.end, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/util/bof.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | /// Returns a parser that succeeds at the start of input. Mirrors [endOfInput] 4 | /// in PetitParser. 5 | Parser startOfInput([String message = 'start of input expected']) => 6 | StartOfInputParser(message); 7 | 8 | /// A parser that succeeds at the start of input. Mirrors [EndOfInputParser] in 9 | /// PetitParser. 10 | class StartOfInputParser extends Parser { 11 | final String message; 12 | 13 | StartOfInputParser(this.message); 14 | 15 | @override 16 | Result parseOn(Context context) { 17 | if (context.position > 0) { 18 | return context.failure(message); 19 | } else { 20 | return context.success(null); 21 | } 22 | } 23 | 24 | @override 25 | int fastParseOn(String buffer, int position) => position > 0 ? -1 : position; 26 | 27 | @override 28 | String toString() => '${super.toString()}[$message]'; 29 | 30 | @override 31 | StartOfInputParser copy() => StartOfInputParser(message); 32 | 33 | @override 34 | bool hasEqualProperties(StartOfInputParser other) => 35 | super.hasEqualProperties(other) && message == other.message; 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/util/drop.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | extension DropParserExtension on Parser { 4 | /// Returns a parser that transforms a successful parse result by removing the 5 | /// elements at [indexes] of a list. Negative indexes can be used to access 6 | /// the elements from the back of the list. 7 | /// 8 | /// For example, the parser `letter().star().drop([0, -1])` returns everything 9 | /// but the first and last letter parsed. For the input `'abc'` it returns 10 | /// `['b']`. 11 | /// 12 | /// Mirrors Parser.permute in PetitParser. 13 | Parser> drop(List indexes) => 14 | castList().map((list) { 15 | final result = list.toList(); 16 | for (final index in indexes 17 | ..sort() 18 | ..reversed) { 19 | result.removeAt((index + list.length) % list.length); 20 | } 21 | return result; 22 | }); 23 | 24 | Parser> drop1(int index) => castList().map((list) { 25 | final result = list.toList(); 26 | result.removeAt((index + list.length) % list.length); 27 | return result; 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/util/elisp_regexp.dart: -------------------------------------------------------------------------------- 1 | import 'package:more/char_matcher.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | import 'unicode.dart'; 5 | 6 | final _letter = UnicodeCharMatcher.letter(); 7 | final _number = UnicodeCharMatcher.number(); 8 | final _currency = UnicodeCharMatcher.symbolCurrency(); 9 | final _otherPunct = UnicodeCharMatcher.punctuationOther(); 10 | final _modifier = UnicodeCharMatcher.symbolModifier(); 11 | 12 | Parser alnum() => 13 | anyCodePoint().where((c) => _letter.match(c) || _number.match(c)); 14 | 15 | Parser alpha() => anyCodePoint().where((c) => _letter.match(c)); 16 | 17 | // Emacs's [:word:] is based on the syntax table. In particular we would care 18 | // about the Org Mode syntax table. It's not feasible to reproduce that here, so 19 | // we approximate it. 20 | Parser word() => anyCodePoint().where((c) => 21 | _letter.match(c) || 22 | _number.match(c) || 23 | _currency.match(c) || 24 | (c > 0xff && (_otherPunct.match(c) || _modifier.match(c))) || 25 | switch (c) { 26 | // % ' · 27 | 0x25 || 0x27 || 0xb7 => true, 28 | _ => false, 29 | }); 30 | -------------------------------------------------------------------------------- /lib/src/util/latex.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | Parser> _latexBlockStart() => 4 | string(r'\begin{') & 5 | (char('}').neg().plusLazy(char('}'))) 6 | .flatten('LaTeX environment expected') & 7 | char('}'); 8 | 9 | class LatexBlockParser extends DelegateParser, List> { 10 | LatexBlockParser() : super(_latexBlockStart()); 11 | 12 | @override 13 | Result> parseOn(Context context) { 14 | final result = delegate.parseOn(context); 15 | if (result is Failure) { 16 | return result; 17 | } 18 | final environment = result.value[1] as String; 19 | final end = '\\end{$environment}'; 20 | final index = result.buffer.indexOf(end, result.position); 21 | if (index == -1) { 22 | return result.failure('Missing end of LaTeX environment'); 23 | } 24 | final content = result.buffer.substring(result.position, index); 25 | return result.success([result.value, content, end], index + end.length); 26 | } 27 | 28 | @override 29 | LatexBlockParser copy() => LatexBlockParser(); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/util/line.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/util/bof.dart'; 2 | import 'package:org_parser/src/util/lookbehind.dart'; 3 | import 'package:petitparser/petitparser.dart'; 4 | 5 | /// Returns a parser that matches the start of a line; this could be the 6 | /// beginning of input, or the position following a line break. 7 | Parser lineStart() => startOfInput() | was(newline()); 8 | 9 | /// Returns a parser that matches the end of a line; this could be the 10 | /// end of input, or a line break. 11 | Parser lineEnd() => newline() | endOfInput(); 12 | 13 | /// Returns a parser that matches everything up to and including the end of the 14 | /// line. 15 | Parser lineTrailing() => any().starLazy(lineEnd()) & lineEnd(); 16 | -------------------------------------------------------------------------------- /lib/src/util/line_breakable.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | import 'unicode.dart'; 4 | 5 | // Characters marked as line-breakable (`?|`) in Emacs's characters.el 6 | 7 | Parser lineBreakable() => 8 | pattern('\u2E80-\u312F' 9 | '\u3190-\u33FF' 10 | '\u3400-\u9FFF' 11 | '\uF900-\uFAFF' 12 | // petitparser doesn't handle codepoints above U+FFFF correctly 13 | // https://github.com/petitparser/dart-petitparser/issues/80#issuecomment-2510372485 14 | // TODO(aaron): revisit this when things change 15 | // '\u{20000}-\u{2FFFF}' 16 | // '\u{30000}-\u{323AF}' 17 | '་།-༒༔ཿ' 18 | '་།༏༐༑༔ཿ') | 19 | codePointRange(from: 0x20000, to: 0x2FFFF) 20 | .map((c) => String.fromCharCodes([c])) | 21 | codePointRange(from: 0x30000, to: 0x323AF) 22 | .map((c) => String.fromCharCodes([c])); 23 | -------------------------------------------------------------------------------- /lib/src/util/local_variables.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/util/util.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | 4 | final _prefix = string('Local Variables:').neg().starString(); 5 | final _suffix = any().starLazy(lineEnd()).flatten('Suffix expected'); 6 | final _start = lineStart().flatten('Leading content expected') & 7 | _prefix & 8 | string('Local Variables:') & 9 | insignificantWhitespace().starString() & 10 | _suffix & 11 | lineEnd(); 12 | 13 | Parser localVariablesParser() => LocalVariablesParser(); 14 | 15 | class LocalVariablesParser extends Parser> { 16 | LocalVariablesParser(); 17 | 18 | @override 19 | LocalVariablesParser copy() => LocalVariablesParser(); 20 | 21 | @override 22 | Result> parseOn(Context context) { 23 | final startResult = _start.parseOn(context); 24 | if (startResult is Failure) { 25 | return startResult; 26 | } 27 | final prefix = startResult.value[1] as String; 28 | final suffix = startResult.value[4] as String; 29 | final contentResult = _rest(prefix, suffix) 30 | .parseOn(Context(context.buffer, startResult.position)); 31 | if (contentResult is Failure) { 32 | return contentResult; 33 | } 34 | return context.success( 35 | [ 36 | // Manually flatten start line 37 | context.buffer.substring(context.position, startResult.position), 38 | ...contentResult.value, 39 | ], 40 | contentResult.position, 41 | ); 42 | } 43 | 44 | Parser _internalLine(String prefix, String suffix) { 45 | final end = suffix.isEmpty ? lineEnd() : string(suffix) & lineEnd(); 46 | return (lineStart() & 47 | string(prefix) & 48 | string('End:') 49 | .neg() 50 | .starLazy(end) 51 | .flatten('Local variable line expected') & 52 | end.flatten('Trailing content expected')) 53 | .drop1(0); 54 | } 55 | 56 | Parser _endLine(String prefix, String suffix) { 57 | final end = 58 | suffix.isEmpty ? lineEnd().and() : string(suffix) & lineEnd().and(); 59 | return (lineStart() & 60 | string(prefix) & 61 | string('End:') & 62 | insignificantWhitespace().starString() & 63 | end) 64 | .drop1(0); 65 | } 66 | 67 | Parser> _rest(String prefix, String suffix) => 68 | _internalLine(prefix, suffix).star() & 69 | _endLine(prefix, suffix).flatten('End line expected'); 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/util/lookbehind.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | /// Returns a parser that attempts to match the given parser from one character 4 | /// behind the actual current position. 5 | Parser was(Parser parser) => LookBehindParser(parser); 6 | 7 | /// A parser that attempts to match the given parser from one character behind 8 | /// the actual current position. 9 | class LookBehindParser extends DelegateParser { 10 | LookBehindParser(super.delegate); 11 | 12 | @override 13 | Result parseOn(Context context) { 14 | final buffer = context.buffer; 15 | final position = context.position; 16 | if (position == 0) { 17 | return context.failure('Cannot look behind start of buffer'); 18 | } 19 | final result = delegate.parseOn(Context(buffer, position - 1)); 20 | if (result is Success) { 21 | return context.success(result.value); 22 | } else { 23 | return result; 24 | } 25 | } 26 | 27 | @override 28 | int fastParseOn(String buffer, int position) { 29 | if (position == 0) { 30 | return -1; 31 | } 32 | final result = delegate.fastParseOn(buffer, position - 1); 33 | return result < 0 ? -1 : position; 34 | } 35 | 36 | @override 37 | LookBehindParser copy() => LookBehindParser(delegate); 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/util/noop.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | /// Returns a parser that fails unconditionally without consuming anything. 4 | /// 5 | /// This is useful for replacing a parser in a sequence when you want the same 6 | /// sequence but without that one parser. 7 | Parser noOpFail() => NoOpParser(false); 8 | 9 | /// A parser that succeeds if [succeed] is true, or otherwise fails 10 | /// unconditionally, both without consuming anything. 11 | class NoOpParser extends Parser { 12 | NoOpParser(this.succeed); 13 | 14 | final bool succeed; 15 | 16 | @override 17 | NoOpParser copy() => NoOpParser(succeed); 18 | 19 | @override 20 | Result parseOn(Context context) { 21 | if (succeed) { 22 | return context.success(null); 23 | } else { 24 | return context.failure('No-op'); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/util/unicode.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | // Is then code (a 16-bit unsigned integer) a UTF-16 lead surrogate. 4 | bool _isLeadSurrogate(int code) => (code & 0xFC00) == 0xD800; 5 | 6 | // Is then code (a 16-bit unsigned integer) a UTF-16 trail surrogate. 7 | bool _isTrailSurrogate(int code) => (code & 0xFC00) == 0xDC00; 8 | 9 | // Combine a lead and a trail surrogate value into a single code point. 10 | int _combineSurrogatePair(int start, int end) { 11 | return 0x10000 + ((start & 0x3FF) << 10) + (end & 0x3FF); 12 | } 13 | 14 | Parser codePointRange({required int from, required int to}) => 15 | anyCodePoint().where((value) => from <= value && value <= to); 16 | 17 | /// Returns a parser that accepts any input element. Like [any] but consumes 18 | /// code point-wise, not code unit-wise, at the cost of some overhead. Returns 19 | /// the numeric value rather than a string. 20 | Parser anyCodePoint([String message = 'input expected']) => 21 | _AnyCodePointParser(message); 22 | 23 | /// A parser that accepts any input element, code point-wise. 24 | class _AnyCodePointParser extends Parser { 25 | _AnyCodePointParser(this.message); 26 | 27 | /// Error message to annotate parse failures with. 28 | final String message; 29 | 30 | @override 31 | Result parseOn(Context context) { 32 | final buffer = context.buffer; 33 | final position = context.position; 34 | if (position < buffer.length) { 35 | var result = buffer.codeUnitAt(position); 36 | var length = 1; 37 | if (_isLeadSurrogate(result) && position + 1 < buffer.length) { 38 | final next = buffer.codeUnitAt(position + 1); 39 | if (_isTrailSurrogate(next)) { 40 | result = _combineSurrogatePair(result, next); 41 | length++; 42 | } 43 | } 44 | return context.success(result, position + length); 45 | } 46 | return context.failure(message); 47 | } 48 | 49 | @override 50 | int fastParseOn(String buffer, int position) { 51 | if (position >= buffer.length) return -1; 52 | return position + 1 < buffer.length && 53 | _isLeadSurrogate(buffer.codeUnitAt(position)) && 54 | _isTrailSurrogate(buffer.codeUnitAt(position + 1)) 55 | ? position + 2 56 | : position + 1; 57 | } 58 | 59 | @override 60 | String toString() => '${super.toString()}[$message]'; 61 | 62 | @override 63 | _AnyCodePointParser copy() => _AnyCodePointParser(message); 64 | 65 | @override 66 | bool hasEqualProperties(_AnyCodePointParser other) => 67 | super.hasEqualProperties(other) && message == other.message; 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/util/util.dart: -------------------------------------------------------------------------------- 1 | export 'block.dart'; 2 | export 'bof.dart'; 3 | export 'drop.dart'; 4 | export 'elisp_regexp.dart'; 5 | export 'indent.dart'; 6 | export 'latex.dart'; 7 | export 'line.dart'; 8 | export 'line_breakable.dart'; 9 | export 'local_variables.dart'; 10 | export 'lookbehind.dart'; 11 | export 'noop.dart'; 12 | export 'unicode.dart'; 13 | export 'whitespace.dart'; 14 | -------------------------------------------------------------------------------- /lib/src/util/whitespace.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | 3 | // Defined outside of grammar to avoid 4 | // https://github.com/petitparser/dart-petitparser/issues/155 5 | Parser insignificantWhitespace() => anyOf(' \t'); 6 | -------------------------------------------------------------------------------- /lib/src/util/zipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:functional_zipper/functional_zipper.dart'; 2 | import 'package:org_parser/org_parser.dart'; 3 | 4 | typedef OrgZipper = ZipperLocation; 5 | 6 | extension ZipperExt 7 | on ZipperLocation { 8 | bool canGoLeft() { 9 | if (path is TopPath) { 10 | return false; 11 | } 12 | final p = path as NodePath; 13 | return p.left.isNotEmpty; 14 | } 15 | 16 | bool canGoRight() { 17 | if (path is TopPath) { 18 | return false; 19 | } 20 | final p = path as NodePath; 21 | return p.right.isNotEmpty; 22 | } 23 | 24 | bool canGoUp() { 25 | return path is NodePath; 26 | } 27 | 28 | bool canGoDown() { 29 | if (!sectionP(node)) { 30 | return false; 31 | } 32 | final t = node as ZS; 33 | final cs = getChildren(t); 34 | return cs.isNotEmpty; 35 | } 36 | 37 | /// Return the root node of this zipper, thereby "applying" any changes made. 38 | ZR commit() { 39 | var location = this; 40 | while (location.path is! TopPath) { 41 | location = location.goUp(); 42 | } 43 | return location.node as ZR; 44 | } 45 | 46 | /// Navigate to the supplied [node], which is presumed to be a child in the 47 | /// tree of this zipper. Returns null if the node is not found. 48 | ZipperLocation? find(ZR node) => 49 | findWhere((n) => identical(n, node)); 50 | 51 | /// Navigate to the node that satisfies [predicate]. Returns null if no such 52 | /// node is not found. 53 | ZipperLocation? findWhere(bool Function(dynamic) predicate) { 54 | var location = this; 55 | while (true) { 56 | if (predicate(location.node)) { 57 | return location; 58 | } 59 | if (location.canGoDown()) { 60 | location = location.goDown(); 61 | continue; 62 | } 63 | if (location.canGoRight()) { 64 | location = location.goRight(); 65 | continue; 66 | } 67 | 68 | retracing: 69 | while (true) { 70 | if (location.canGoUp()) { 71 | location = location.goUp(); 72 | } else { 73 | return null; 74 | } 75 | if (location.canGoRight()) { 76 | location = location.goRight(); 77 | break retracing; 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: org_parser 2 | description: A pure-Dart parser for Emacs Org Mode (https://orgmode.org) markup 3 | version: 9.7.3 4 | homepage: https://github.com/amake/org_parser 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | petitparser: ^6.0.1 11 | functional_zipper: ^0.0.2 12 | more: ^4.4.0 13 | 14 | dev_dependencies: 15 | lints: ^5.0.0 16 | test: ^1.12.0 17 | collection: ^1.0.0 18 | -------------------------------------------------------------------------------- /test/bin/roundtrip.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:org_parser/org_parser.dart'; 4 | 5 | void main(List args) { 6 | for (final arg in args) { 7 | final doc = File(arg).readAsStringSync(); 8 | final parsed = OrgDocument.parse(doc); 9 | stdout.write(parsed.toMarkup()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/edit_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/org_parser.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('edit', () { 6 | final doc = OrgDocument.parse('''* TODO [#A] foo bar 7 | 1'''); 8 | final zipper = doc.edit(); 9 | final sectionLoc = zipper.goDown(); 10 | expect(sectionLoc.node, isA()); 11 | final headlineLoc = sectionLoc.goDown(); 12 | expect(headlineLoc.node, isA()); 13 | final contentLoc = headlineLoc.goRight(); 14 | expect(contentLoc.node, isA()); 15 | final paragraphLoc = contentLoc.goDown(); 16 | expect(paragraphLoc.node, isA()); 17 | final paragraphContentLoc = paragraphLoc.goDown(); 18 | expect(paragraphContentLoc.node, isA()); 19 | final textLoc = paragraphContentLoc.goDown(); 20 | expect(textLoc.node, isA()); 21 | final edited = textLoc.replace(OrgPlainText('2')).commit(); 22 | expect(edited.toMarkup(), '''* TODO [#A] foo bar 23 | 2'''); 24 | }); 25 | test('find node', () { 26 | final doc = OrgDocument.parse('''* TODO [#A] foo bar 27 | *baz*'''); 28 | final found = doc.find((_) => true)!; 29 | var zipper = doc.editNode(found.node)!; 30 | zipper = zipper.replace( 31 | found.node.copyWith(leadingDecoration: '/', trailingDecoration: '/')); 32 | final edited = zipper.commit(); 33 | expect(edited.toMarkup(), '''* TODO [#A] foo bar 34 | /baz/'''); 35 | }); 36 | test('find by predicate', () { 37 | final doc = OrgDocument.parse('''* TODO [#A] foo bar 38 | *baz*'''); 39 | var zipper = doc.edit().findWhere((node) => node is OrgMarkup)!; 40 | final found = zipper.node as OrgMarkup; 41 | zipper = zipper.replace( 42 | found.copyWith(leadingDecoration: '/', trailingDecoration: '/')); 43 | final edited = zipper.commit(); 44 | expect(edited.toMarkup(), '''* TODO [#A] foo bar 45 | /baz/'''); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/example-gold.txt: -------------------------------------------------------------------------------- 1 | TODO 2 | foo bar 3 | baz buzz 4 | I'm going to go fishing 5 | I'm going to eat lunch 6 | I'm going to take a nap 7 | * TODO [#A] foo bar 8 | bazinga 9 | -------------------------------------------------------------------------------- /test/file_link/model_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/file_link/file_link.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('file link', () { 6 | test('file: relative', () { 7 | final link = OrgFileLink.parse('file:foo.org'); 8 | expect(link.isLocal, isFalse); 9 | expect(link.isRelative, isTrue); 10 | expect(link.scheme, 'file:'); 11 | expect(link.body, 'foo.org'); 12 | expect(link.extra, isNull); 13 | }); 14 | test('file: local', () { 15 | final link = OrgFileLink.parse('file:::*'); 16 | expect(link.isLocal, isTrue); 17 | expect(link.isRelative, isTrue); 18 | expect(link.scheme, 'file:'); 19 | expect(link.body, ''); 20 | expect(link.extra, '*'); 21 | }); 22 | test('attachment: relative', () { 23 | final link = OrgFileLink.parse('attachment:foo.org'); 24 | expect(link.isLocal, isFalse); 25 | expect(link.isRelative, isTrue); 26 | expect(link.scheme, 'attachment:'); 27 | expect(link.body, 'foo.org'); 28 | expect(link.extra, isNull); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/file_link/parser_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/file_link/file_link.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('file link', () { 7 | final parser = orgFileLink; 8 | group('with scheme', () { 9 | test('absolute path', () { 10 | var result = parser.parse('file:/home/dominik/images/jupiter.jpg'); 11 | var link = result.value as OrgFileLink; 12 | expect(link.scheme, 'file:'); 13 | expect(link.body, '/home/dominik/images/jupiter.jpg'); 14 | expect(link.extra, isNull); 15 | expect(link.isRelative, isFalse); 16 | expect(link.isLocal, isFalse); 17 | }); 18 | test('relative path', () { 19 | final result = parser.parse('file:papers/last.pdf'); 20 | final link = result.value as OrgFileLink; 21 | expect(link.scheme, 'file:'); 22 | expect(link.body, 'papers/last.pdf'); 23 | expect(link.extra, isNull); 24 | expect(link.isRelative, isTrue); 25 | expect(link.isLocal, isFalse); 26 | }); 27 | }); 28 | group('with extra', () { 29 | test('other file', () { 30 | final result = parser.parse('file:projects.org::some words'); 31 | final link = result.value as OrgFileLink; 32 | expect(link.scheme, 'file:'); 33 | expect(link.body, 'projects.org'); 34 | expect(link.extra, 'some words'); 35 | expect(link.isRelative, isTrue); 36 | expect(link.isLocal, isFalse); 37 | }); 38 | test('local file', () { 39 | final result = parser.parse('file:::#custom-id'); 40 | final link = result.value as OrgFileLink; 41 | expect(link.scheme, 'file:'); 42 | expect(link.body, ''); 43 | expect(link.extra, '#custom-id'); 44 | expect(link.isRelative, isTrue); 45 | expect(link.isLocal, isTrue); 46 | }); 47 | }); 48 | group('without scheme', () { 49 | test('absolute path', () { 50 | final result = parser.parse('/home/dominik/images/jupiter.jpg'); 51 | final link = result.value as OrgFileLink; 52 | expect(link.scheme, isNull); 53 | expect(link.body, '/home/dominik/images/jupiter.jpg'); 54 | expect(link.extra, isNull); 55 | expect(link.isRelative, isFalse); 56 | expect(link.isLocal, isFalse); 57 | }); 58 | test('relative path', () { 59 | final result = parser.parse('./papers/last.pdf'); 60 | final link = result.value as OrgFileLink; 61 | expect(link.scheme, isNull); 62 | expect(link.body, './papers/last.pdf'); 63 | expect(link.extra, isNull); 64 | expect(link.isRelative, isTrue); 65 | expect(link.isLocal, isFalse); 66 | }); 67 | }); 68 | group('non-files', () { 69 | test('https', () { 70 | final result = parser.parse('https://example.com'); 71 | expect(result, isA()); 72 | }); 73 | test('mailto', () { 74 | final result = parser.parse('mailto:me@example.com'); 75 | expect(result, isA()); 76 | }); 77 | }); 78 | test('factory', () { 79 | final link = OrgFileLink.parse('file:papers/last.pdf'); 80 | expect(link.scheme, 'file:'); 81 | expect(link.body, 'papers/last.pdf'); 82 | expect(link.extra, isNull); 83 | expect(link.isRelative, isTrue); 84 | try { 85 | OrgFileLink.parse('https://example.com'); 86 | fail('OrgFileLink parser should not accept HTTPS link'); 87 | } on ParserException { 88 | // OK 89 | } 90 | }); 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /test/matchers.dart: -------------------------------------------------------------------------------- 1 | import 'package:petitparser/petitparser.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | TypeMatcher> isSeparatedList({ 5 | List elements = const [], 6 | List separators = const [], 7 | }) => 8 | isA>() 9 | .having((list) => list.elements, 'elements', elements) 10 | .having((list) => list.separators, 'separators', separators); 11 | -------------------------------------------------------------------------------- /test/org/grammar/affiliated_keyword_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('affiliated keyword', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.affiliatedKeyword()).end(); 9 | test('simple', () { 10 | final result = parser.parse('#+blah: foo'); 11 | expect(result.value, [ 12 | '', 13 | ['#+blah:', ' foo'], 14 | '' 15 | ]); 16 | }); 17 | test('trailing', () { 18 | final result = parser.parse('#+blah: foo\n\n'); 19 | expect(result.value, [ 20 | '', 21 | ['#+blah:', ' foo'], 22 | '\n\n' 23 | ]); 24 | }); 25 | test('empty value', () { 26 | final result = parser.parse('#+blah:'); 27 | expect(result.value, [ 28 | '', 29 | ['#+blah:', ''], 30 | '' 31 | ]); 32 | }); 33 | test('indented', () { 34 | final result = parser.parse(' #+blah: foo'); 35 | expect(result.value, [ 36 | ' ', 37 | ['#+blah:', ' foo'], 38 | '' 39 | ]); 40 | }); 41 | test('not at beginning of line', () { 42 | final result = parser.parse('''a #+blah'''); 43 | expect(result, isA()); 44 | }); 45 | test('missing colon', () { 46 | final result = parser.parse('''#+blah'''); 47 | expect(result, isA()); 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/org/grammar/block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('block', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.block()).end(); 9 | test('lower case', () { 10 | final result = parser.parse('''#+begin_src sh 11 | echo 'foo' 12 | rm bar 13 | #+end_src'''); 14 | expect(result.value, [ 15 | '', 16 | [ 17 | [ 18 | '#+begin_src', 19 | [' ', 'sh'], 20 | '\n' 21 | ], 22 | ' echo \'foo\'\n rm bar\n', 23 | ['', '#+end_src'] 24 | ], 25 | '' 26 | ]); 27 | }); 28 | test('mismatched case', () { 29 | final result = parser.parse('''#+BEGIN_SRC sh 30 | echo 'foo' 31 | rm bar 32 | #+EnD_sRC 33 | '''); 34 | expect(result.value, [ 35 | '', 36 | [ 37 | [ 38 | '#+BEGIN_SRC', 39 | [' ', 'sh'], 40 | '\n' 41 | ], 42 | ' echo \'foo\'\n rm bar\n', 43 | ['', '#+EnD_sRC'] 44 | ], 45 | '\n' 46 | ]); 47 | }); 48 | test('no language', () { 49 | final result = parser.parse('''#+begin_src 50 | echo 'foo' 51 | rm bar 52 | #+end_src 53 | '''); 54 | expect(result.value, [ 55 | '', 56 | [ 57 | ['#+begin_src', null, '\n'], 58 | ' echo \'foo\'\n rm bar\n', 59 | ['', '#+end_src'] 60 | ], 61 | '\n' 62 | ]); 63 | }); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /test/org/grammar/citation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('citation', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.citation()).end(); 9 | test('simple', () { 10 | final result = parser.parse('[cite:@key]'); 11 | expect(result.value, ['[cite', null, ':', '@key', ']']); 12 | }); 13 | test('with style', () { 14 | final result = parser.parse('[cite/mystyle:@key]'); 15 | expect(result.value, [ 16 | '[cite', 17 | ['/', 'mystyle'], 18 | ':', 19 | '@key', 20 | ']' 21 | ]); 22 | }); 23 | test('multiple keys', () { 24 | final result = parser.parse('[cite:@key1;@key2;@key3]'); 25 | expect(result.value, ['[cite', null, ':', '@key1;@key2;@key3', ']']); 26 | }); 27 | test('prefix and suffix', () { 28 | final result = 29 | parser.parse('[cite:common pref ;foo @key bar; common suff]'); 30 | expect(result.value, 31 | ['[cite', null, ':', 'common pref ;foo @key bar; common suff', ']']); 32 | }); 33 | test('invalid', () { 34 | final result = parser.parse('[cite:key]'); 35 | expect(result, isA()); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/org/grammar/comment_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Comment', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.comment()).end(); 9 | test('simple', () { 10 | final result = parser.parse('''# foo bar'''); 11 | expect(result.value, ['', '# ', 'foo bar', '']); 12 | }); 13 | test('indented', () { 14 | final result = parser.parse(''' # foo bar'''); 15 | expect(result.value, [' ', '# ', 'foo bar', '']); 16 | }); 17 | test('trailing', () { 18 | final result = parser.parse(''' # foo bar 19 | 20 | '''); 21 | expect(result.value, [' ', '# ', 'foo bar', '\n\n']); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/org/grammar/dynamic_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('dynamic block', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.dynamicBlock()).end(); 9 | test('normal', () { 10 | final result = 11 | parser.parse('''#+BEGIN: myblock :parameter1 value1 :parameter2 value2 12 | foobar 13 | #+END: 14 | '''); 15 | expect(result.value, [ 16 | '', 17 | [ 18 | [ 19 | '#+BEGIN:', 20 | [' ', 'myblock'], 21 | ' :parameter1 value1 :parameter2 value2\n' 22 | ], 23 | ' foobar\n', 24 | ['', '#+END:'] 25 | ], 26 | '\n' 27 | ]); 28 | }); 29 | test('lower case', () { 30 | final result = 31 | parser.parse('''#+begin: myblock :parameter1 value1 :parameter2 value2 32 | foobar 33 | #+end: 34 | '''); 35 | expect(result.value, [ 36 | '', 37 | [ 38 | [ 39 | '#+begin:', 40 | [' ', 'myblock'], 41 | ' :parameter1 value1 :parameter2 value2\n' 42 | ], 43 | ' foobar\n', 44 | ['', '#+end:'] 45 | ], 46 | '\n' 47 | ]); 48 | }); 49 | test('invalid', () { 50 | final result = parser.parse('''#+BEGIN: 51 | foobar 52 | #+END: 53 | '''); 54 | expect(result, isA()); 55 | }); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /test/org/grammar/entity_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('entity', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.entity()).end(); 9 | test('there4', () { 10 | final result = parser.parse(r'\there4'); 11 | expect(result.value, [r'\', 'there4', '']); 12 | }); 13 | test('sup valid', () { 14 | final result = parser.parse(r'\sup1'); 15 | expect(result.value, [r'\', 'sup1', '']); 16 | }); 17 | test('sup invalid', () { 18 | final result = parser.parse(r'\sup5'); 19 | expect(result, isA()); 20 | }); 21 | test('valid frac', () { 22 | final result = parser.parse(r'\frac12'); 23 | expect(result.value, [r'\', 'frac12', '']); 24 | }); 25 | test('invalid frac', () { 26 | final result = parser.parse(r'\frac15'); 27 | expect(result, isA()); 28 | }); 29 | test('arbitrary alphabetical', () { 30 | final result = parser.parse(r'\foobar'); 31 | expect(result.value, [r'\', 'foobar', '']); 32 | }); 33 | test('arbitrary alphanumeric', () { 34 | final result = parser.parse(r'\foobar2'); 35 | expect(result, isA()); 36 | }); 37 | test('with terminator', () { 38 | final result = parser.parse(r'\foobar{}'); 39 | expect(result.value, [r'\', 'foobar', '{}']); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/org/grammar/fixed_width_area_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('fixed-width area', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.fixedWidthArea()).end(); 9 | test('single line', () { 10 | final result = parser.parse(' : foo'); 11 | expect(result.value, [ 12 | [ 13 | [' ', ': ', 'foo'] 14 | ], 15 | '' 16 | ]); 17 | }); 18 | test('multiple lines', () { 19 | final result = parser.parse(''' : foo 20 | : bar'''); 21 | expect(result.value, [ 22 | [ 23 | [' ', ': ', 'foo\n'], 24 | [' ', ': ', 'bar'] 25 | ], 26 | '' 27 | ]); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/org/grammar/footnote_reference_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('footnote reference', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.footnoteReference()).end(); 9 | test('numeric', () { 10 | final result = parser.parse('[fn:1]'); 11 | expect(result.value, ['[fn:', '1', ']']); 12 | }); 13 | test('alphanumeric', () { 14 | final result = parser.parse('[fn:abc123]'); 15 | expect(result.value, ['[fn:', 'abc123', ']']); 16 | }); 17 | test('with definition', () { 18 | final result = parser.parse('[fn:abc123: who what why]'); 19 | expect(result.value, [ 20 | '[fn:', 21 | 'abc123', 22 | ':', 23 | [' who what why'], 24 | ']' 25 | ]); 26 | }); 27 | test('with definition with formatting', () { 28 | final result = parser.parse('[fn:abc123: who *what* why]'); 29 | expect(result.value, [ 30 | '[fn:', 31 | 'abc123', 32 | ':', 33 | [ 34 | ' who ', 35 | ['*', 'what', '*'], 36 | ' why' 37 | ], 38 | ']' 39 | ]); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/org/grammar/footnote_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('footnote', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.footnote()).end(); 9 | test('simple', () { 10 | final result = parser.parse('[fn:1] foo *bar*'); 11 | expect(result.value, [ 12 | ['[fn:', '1', ']'], 13 | [ 14 | ' foo ', 15 | ['*', 'bar', '*'] 16 | ], 17 | '' 18 | ]); 19 | }); 20 | test('multiple lines', () { 21 | final result = parser.parse('''[fn:1] foo *bar* 22 | baz bazinga 23 | 24 | '''); 25 | expect(result.value, [ 26 | ['[fn:', '1', ']'], 27 | [ 28 | ' foo ', 29 | ['*', 'bar', '*'], 30 | '\nbaz bazinga' 31 | ], 32 | '\n\n' 33 | ]); 34 | }); 35 | test('indented', () { 36 | final result = parser.parse(' [fn:1] foo *bar*'); 37 | expect(result, isA(), reason: 'Indent not allowed'); 38 | }); 39 | test('complex reference', () { 40 | final result = parser.parse('[fn:1: blah] foo *bar*'); 41 | expect(result, isA(), reason: 'Only simple references allowed'); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/org/grammar/grammar_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: inference_failure_on_collection_literal 2 | 3 | import 'package:org_parser/src/org/org.dart'; 4 | import 'package:petitparser/petitparser.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | group('document grammar', () { 9 | final grammar = OrgGrammarDefinition().build(); 10 | test('parse content', () { 11 | final result = grammar.parse('''foo 12 | bar 13 | '''); 14 | expect(result.value, ['foo\nbar\n', []]); 15 | }); 16 | test('parse an empty header before a regular header', () { 17 | final result = grammar.parse('''**${' '} 18 | * foo'''); 19 | expect(result.value, [ 20 | null, 21 | [ 22 | [ 23 | [ 24 | ['**', ' '], 25 | null, 26 | null, 27 | null, 28 | null, 29 | '\n', 30 | ], 31 | null, 32 | ], 33 | [ 34 | [ 35 | ['*', ' '], 36 | null, 37 | null, 38 | 'foo', 39 | null, 40 | null 41 | ], 42 | null 43 | ] 44 | ] 45 | ]); 46 | }); 47 | test('parse an almost-header before content', () { 48 | final result = grammar.parse('''* 49 | foo'''); 50 | expect(result.value, ['*\nfoo', []]); 51 | }); 52 | test('parse a section', () { 53 | final result = grammar.parse('''* Title 54 | Content1 55 | Content2'''); 56 | expect(result.value, [ 57 | null, 58 | [ 59 | [ 60 | [ 61 | ['*', ' '], 62 | null, 63 | null, 64 | 'Title', 65 | null, 66 | '\n' 67 | ], 68 | ' Content1\n Content2' 69 | ] 70 | ] 71 | ]); 72 | }); 73 | test('valid headers', () { 74 | for (final valid in [ 75 | '* ', 76 | '** DONE', 77 | '*** Some e-mail', 78 | '**** TODO [#A] COMMENT Title :tag:a2%:', 79 | ]) { 80 | expect(grammar.parse(valid), isA>()); 81 | } 82 | }); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /test/org/grammar/greater_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('greater block', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.greaterBlock()).end(); 9 | test('lower case', () { 10 | final result = parser.parse('''#+begin_quote 11 | foo *bar* 12 | #+end_quote'''); 13 | expect(result.value, [ 14 | '', 15 | [ 16 | ['#+begin_quote', '\n'], 17 | ' foo *bar*\n', 18 | ['', '#+end_quote'] 19 | ], 20 | '' 21 | ]); 22 | }); 23 | test('mismatched case', () { 24 | final result = parser.parse('''#+BEGIN_QUOTE 25 | foo /bar/ 26 | #+EnD_qUOtE 27 | '''); 28 | expect(result.value, [ 29 | '', 30 | [ 31 | ['#+BEGIN_QUOTE', '\n'], 32 | ' foo /bar/\n', 33 | ['', '#+EnD_qUOtE'] 34 | ], 35 | '\n' 36 | ]); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/org/grammar/headline_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import '../../matchers.dart'; 6 | 7 | void main() { 8 | group('headline', () { 9 | final grammarDefinion = OrgGrammarDefinition(); 10 | final parser = grammarDefinion.buildFrom(grammarDefinion.headline()); 11 | test('parse a headline', () { 12 | final result = parser.parse('* Title'); 13 | expect( 14 | result.value, 15 | [ 16 | ['*', ' '], 17 | null, 18 | null, 19 | 'Title', 20 | null, 21 | null 22 | ], 23 | ); 24 | }); 25 | test('parse almost-a-header', () { 26 | final result = parser.parse('**'); 27 | expect(result, isA()); 28 | }); 29 | test('parse just an empty header', () { 30 | final result = parser.parse('* '); 31 | expect( 32 | result.value, 33 | [ 34 | ['*', ' '], 35 | null, 36 | null, 37 | null, 38 | null, 39 | null 40 | ], 41 | ); 42 | }); 43 | test('parse a todo header', () { 44 | final result = parser.parse('* TODO Title'); 45 | expect( 46 | result.value, 47 | [ 48 | ['*', ' '], 49 | ['TODO', ' '], 50 | null, 51 | 'Title', 52 | null, 53 | null 54 | ], 55 | ); 56 | }); 57 | test('parse a complex header', () { 58 | final result = parser.parse('** TODO [#A] Title foo bar :biz:baz:'); 59 | expect(result.value, [ 60 | ['**', ' '], 61 | ['TODO', ' '], 62 | ['[#', 'A', '] '], 63 | 'Title foo bar ', 64 | [ 65 | ':', 66 | isSeparatedList(elements: [ 67 | 'biz', 68 | 'baz', 69 | ], separators: [ 70 | ':' 71 | ]), 72 | ':', 73 | null, 74 | ], 75 | null 76 | ]); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/org/grammar/horizontal_rule_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('horizontal rule', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.horizontalRule()).end(); 9 | test('minimal', () { 10 | final result = parser.parse('-----'); 11 | expect(result.value, ['', '-----', '']); 12 | }); 13 | test('indented', () { 14 | final result = parser.parse(' -----'); 15 | expect(result.value, [' ', '-----', '']); 16 | }); 17 | test('trailing', () { 18 | final result = parser.parse('''-----${' '} 19 | 20 | '''); 21 | expect(result.value, ['', '-----', ' \n\n']); 22 | }); 23 | test('long', () { 24 | final result = parser.parse('----------------'); 25 | expect(result.value, ['', '----------------', '']); 26 | }); 27 | test('too short', () { 28 | final result = parser.parse('----'); 29 | expect(result, isA()); 30 | }); 31 | test('trailing garbage', () { 32 | final result = parser.parse('----- a'); 33 | expect(result, isA()); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /test/org/grammar/inline_src_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('inline src', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.inlineSourceBlock()).end(); 9 | test('no args', () { 10 | final result = parser.parse('''src_sh{echo "foo"}'''); 11 | expect(result.value, ['src_', 'sh', null, '{echo "foo"}']); 12 | }); 13 | test('with args', () { 14 | final result = parser.parse('''src_sh[:exports code]{echo "foo"}'''); 15 | expect(result.value, ['src_', 'sh', '[:exports code]', '{echo "foo"}']); 16 | }); 17 | test('missing lang', () { 18 | final result = parser.parse('''src_[:exports code]{echo "foo"}'''); 19 | expect(result, isA()); 20 | }); 21 | test('args contains bracket', () { 22 | final result = parser.parse(r'''src_sh[:var foo="[a]"]{echo $foo}'''); 23 | expect(result, isA()); 24 | }); 25 | test('body contains brace', () { 26 | final result = parser.parse(r'''src_sh{echo "${foo}"}'''); 27 | expect(result, isA()); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/org/grammar/link_target_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('link target', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.linkTarget()).end(); 9 | test('single character', () { 10 | final result = parser.parse('<>'); 11 | expect(result.value, ['<<', '!', '>>']); 12 | }); 13 | test('multiple words', () { 14 | final result = parser.parse('<>'); 15 | expect(result.value, ['<<', 'foo bar', '>>']); 16 | }); 17 | test('too few brackets', () { 18 | final result = parser.parse(''); 19 | expect(result, isA()); 20 | }); 21 | test('too many brackets', () { 22 | final result = parser.parse('<<>>'); 23 | expect(result, isA()); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/org/grammar/link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('link', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.link()).end(); 9 | test('with description', () { 10 | final result = parser.parse('[[http://example.com][example]]'); 11 | expect(result.value, [ 12 | '[', 13 | ['[', 'http://example.com', ']'], 14 | ['[', 'example', ']'], 15 | ']' 16 | ]); 17 | }); 18 | test('brackets in location', () { 19 | final result = 20 | parser.parse('[[*\\[wtf\\] what?][[lots][of][boxes]\u200b]]'); 21 | expect(result.value, [ 22 | '[', 23 | ['[', '*[wtf] what?', ']'], 24 | ['[', '[lots][of][boxes]', ']'], 25 | ']' 26 | ]); 27 | }); 28 | test('bare HTTP URL', () { 29 | final result = parser.parse('http://example.com'); 30 | expect(result.value, 'http://example.com'); 31 | }); 32 | test('bare HTTPS URL', () { 33 | final result = parser.parse('https://example.com'); 34 | expect(result.value, 'https://example.com'); 35 | }); 36 | test('bare file URL', () { 37 | final result = parser.parse('file:example.txt'); 38 | expect(result.value, 'file:example.txt'); 39 | }); 40 | test('bare attachment URL', () { 41 | final result = parser.parse('attachment:example.txt'); 42 | expect(result.value, 'attachment:example.txt'); 43 | }); 44 | test('arbitrary protocol', () { 45 | final result = parser.parse('foobar://example.com'); 46 | expect(result, isA()); 47 | }); 48 | test('nested markup', () { 49 | final result = parser.parse('[[http://example.com][*example*]]'); 50 | expect( 51 | result.value, 52 | [ 53 | '[', 54 | ['[', 'http://example.com', ']'], 55 | ['[', '*example*', ']'], 56 | ']' 57 | ], 58 | reason: 'description content parsed on separate layer'); 59 | }); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /test/org/grammar/local_variables_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('local variables', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.localVariables()).end(); 9 | test('simple', () { 10 | final result = parser.parse('''# Local Variables: 11 | # foo: bar 12 | # End: '''); 13 | expect(result.value, [ 14 | [ 15 | '# Local Variables:\n', 16 | [ 17 | ['# ', 'foo: bar', '\n'] 18 | ], 19 | '# End: ', 20 | ], 21 | '' 22 | ]); 23 | }); 24 | test('with suffix', () { 25 | final result = parser.parse('''# Local Variables: # 26 | # foo: bar # 27 | # End: #'''); 28 | expect(result.value, [ 29 | [ 30 | '# Local Variables: #\n', 31 | [ 32 | ['# ', 'foo: bar ', '#\n'] 33 | ], 34 | '# End: #' 35 | ], 36 | '' 37 | ]); 38 | }); 39 | test('bad prefix', () { 40 | final result = parser.parse('''# Local Variables: 41 | ## foo: bar 42 | # End:'''); 43 | expect(result, isA()); 44 | }); 45 | test('bad suffix', () { 46 | final result = parser.parse('''/* Local Variables: */ 47 | /* foo: bar */ 48 | /* End: **/'''); 49 | expect(result, isA()); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/org/grammar/macro_reference_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('macro reference', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.macroReference()).end(); 9 | test('with args', () { 10 | final result = parser.parse('{{{name(arg1, arg2)}}}'); 11 | expect(result.value, ['{{{', 'name', '(arg1, arg2)', '}}}']); 12 | }); 13 | test('simple', () { 14 | final result = parser.parse('{{{foobar}}}'); 15 | expect(result.value, ['{{{', 'foobar', '', '}}}']); 16 | }); 17 | test('empty', () { 18 | final result = parser.parse('{{{}}}'); 19 | expect(result, isA()); 20 | }); 21 | test('invalid key', () { 22 | final result = parser.parse('{{{0abc}}}'); 23 | expect(result, isA()); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/org/grammar/markup_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('markup', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.markups()).end(); 9 | test('bad pre and post chars', () { 10 | final result = parser.parse('''a/b 11 | c/d'''); 12 | expect(result, isA()); 13 | }); 14 | test('bad post char', () { 15 | final result = parser.parse('''a /b 16 | c/d'''); 17 | expect(result, isA()); 18 | }); 19 | test('bad pre char', () { 20 | final result = parser.parse('''a/b 21 | c/ d'''); 22 | expect(result, isA()); 23 | }); 24 | test('single char', () { 25 | final result = parser.parse('/a/'); 26 | expect(result.value, ['/', 'a', '/']); 27 | }); 28 | test('single word', () { 29 | final result = parser.parse('/abc/'); 30 | expect(result.value, ['/', 'abc', '/']); 31 | }); 32 | test('multiple words', () { 33 | final result = parser.parse('/a b/'); 34 | expect(result.value, ['/', 'a b', '/']); 35 | }); 36 | test('empty', () { 37 | final result = parser.parse('//'); 38 | expect(result, isA()); 39 | }); 40 | test('single comma', () { 41 | final result = parser.parse('~,~'); 42 | expect(result.value, ['~', ',', '~']); 43 | }); 44 | test('single apostrophe', () { 45 | final result = parser.parse("~'~"); 46 | expect(result.value, ['~', "'", '~']); 47 | }); 48 | test('with delimiters inside', () { 49 | final result = parser.parse('=+LEVEL=3+boss-TODO​="DONE"='); 50 | expect(result.value, ['=', '+LEVEL=3+boss-TODO​="DONE"', '=']); 51 | }); 52 | test('with line break', () { 53 | final result = parser.parse('''+foo 54 | bar+'''); 55 | expect(result.value, ['+', 'foo\nbar', '+']); 56 | }); 57 | test('nested markup', () { 58 | final result = parser.parse("~foo *bar* baz~"); 59 | expect(result.value, ['~', 'foo *bar* baz', '~'], 60 | reason: 'body is parsed in separate phase'); 61 | }); 62 | test('too many line breaks', () { 63 | final result = parser.parse('''+foo 64 | 65 | bar+'''); 66 | expect(result, isA()); 67 | }); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /test/org/grammar/paragraph_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('paragraph', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.paragraph()).end(); 9 | test('with line break', () { 10 | final result = parser.parse('''foo bar 11 | biz baz'''); 12 | expect(result.value, [ 13 | '', 14 | ['foo bar\nbiz baz'], 15 | '' 16 | ]); 17 | }); 18 | test('too many line breaks', () { 19 | final result = parser.parse('''foo bar 20 | 21 | biz baz'''); 22 | expect(result, isA()); 23 | }); 24 | test('with inline objects', () { 25 | final result = 26 | parser.parse('''go to [[http://example.com][example]] for *fun*, 27 | maybe'''); 28 | expect(result.value, [ 29 | '', 30 | [ 31 | 'go to ', 32 | [ 33 | '[', 34 | ['[', 'http://example.com', ']'], 35 | ['[', 'example', ']'], 36 | ']' 37 | ], 38 | ' for ', 39 | ['*', 'fun', '*'], 40 | ',\nmaybe' 41 | ], 42 | '' 43 | ]); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /test/org/grammar/pgp_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('PGP block', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.pgpBlock()).end(); 9 | test('simple', () { 10 | final result = parser.parse('''-----BEGIN PGP MESSAGE----- 11 | 12 | jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O 13 | X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY= 14 | =chda 15 | -----END PGP MESSAGE----- 16 | '''); 17 | expect(result.value, [ 18 | '', 19 | [ 20 | '-----BEGIN PGP MESSAGE-----', 21 | '\n\n' 22 | 'jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O\n' 23 | 'X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY=\n' 24 | '=chda\n', 25 | '-----END PGP MESSAGE-----', 26 | ], 27 | '\n' 28 | ]); 29 | }); 30 | test('indented', () { 31 | final result = parser.parse(''' -----BEGIN PGP MESSAGE----- 32 | 33 | jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O 34 | X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY= 35 | =chda 36 | -----END PGP MESSAGE----- 37 | 38 | '''); 39 | expect(result.value, [ 40 | ' ', 41 | [ 42 | '-----BEGIN PGP MESSAGE-----', 43 | '\n\n' 44 | ' jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O\n' 45 | ' X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY=\n' 46 | ' =chda\n' 47 | ' ', 48 | '-----END PGP MESSAGE-----', 49 | ], 50 | '\n\n' 51 | ]); 52 | }); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /test/org/grammar/planning_entry.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: inference_failure_on_collection_literal 2 | 3 | import 'package:org_parser/src/org/org.dart'; 4 | import 'package:petitparser/petitparser.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test('planning entry', () { 9 | final grammar = OrgContentGrammarDefinition(); 10 | final parser = grammar.buildFrom(grammar.planningEntry()).end(); 11 | final result = 12 | parser.parse('CLOCK: [2021-02-16 Tue 21:40]--[2021-02-16 Tue 21:40]'); 13 | expect(result.value, [ 14 | 'CLOCK:', 15 | ' ', 16 | [ 17 | [ 18 | '[', 19 | ['2021', '-', '02', '-', '16', 'Tue'], 20 | ['21', ':', '40'], 21 | [], 22 | ']' 23 | ], 24 | '--', 25 | [ 26 | '[', 27 | ['2021', '-', '02', '-', '16', 'Tue'], 28 | ['21', ':', '40'], 29 | [], 30 | ']' 31 | ] 32 | ], 33 | ]); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /test/org/grammar/property_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('property', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.property()).end(); 9 | test('simple', () { 10 | final result = parser.parse(':foo: bar'); 11 | expect(result.value, [ 12 | '', 13 | [':', 'foo', ':'], 14 | ' bar', 15 | '' 16 | ]); 17 | }); 18 | test('missing value', () { 19 | final result = parser.parse(':foo:'); 20 | expect(result, isA()); 21 | }); 22 | test('missing delimiter', () { 23 | final result = parser.parse(':foo:blah'); 24 | expect(result, isA(), reason: 'Delimiting space required'); 25 | }); 26 | test('line break', () { 27 | final result = parser.parse(''':foo: 28 | bar'''); 29 | expect(result, isA(), reason: 'Value must be on same line'); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/org/grammar/radio_link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('radio link', () { 7 | group('none defined', () { 8 | final grammar = OrgContentGrammarDefinition(); 9 | final parser = grammar.buildFrom(grammar.radioLink()).end(); 10 | test('non-empty fails', () { 11 | final result = parser.parse('foo'); 12 | expect(result, isA()); 13 | }); 14 | test('empty fails', () { 15 | final result = parser.parse(''); 16 | expect(result, isA()); 17 | }); 18 | }); 19 | group('some defined', () { 20 | final grammar = OrgContentGrammarDefinition(radioTargets: ['foo', 'bar']); 21 | test('solitary', () { 22 | final parser = grammar.buildFrom(grammar.radioLink()).end(); 23 | var result = parser.parse('foo'); 24 | expect(result.value, [null, 'foo', null]); 25 | result = parser.parse('bar'); 26 | expect(result.value, [null, 'bar', null]); 27 | }); 28 | test('case-insensitive', () { 29 | final parser = grammar.buildFrom(grammar.radioLink()).end(); 30 | var result = parser.parse('Foo'); 31 | expect(result.value, [null, 'Foo', null]); 32 | result = parser.parse('baR'); 33 | expect(result.value, [null, 'baR', null]); 34 | }); 35 | group('in context', () { 36 | final parser = 37 | grammar.buildFrom(any() & grammar.radioLink() & any()).end(); 38 | test('spaces around', () { 39 | var result = parser.parse(' foo '); 40 | expect(result.value, [ 41 | ' ', 42 | [anything, 'foo', anything], 43 | ' ' 44 | ]); 45 | result = parser.parse(' bar '); 46 | expect(result.value, [ 47 | ' ', 48 | [anything, 'bar', anything], 49 | ' ' 50 | ]); 51 | }); 52 | test('non-alphanumeric around', () { 53 | final result = parser.parse('.foo,'); 54 | expect(result.value, [ 55 | '.', 56 | [anything, 'foo', anything], 57 | ',' 58 | ]); 59 | }); 60 | test('line-breaking around', () { 61 | final result = parser.parse('あfooお'); 62 | expect(result.value, [ 63 | 'あ', 64 | [anything, 'foo', anything], 65 | 'お' 66 | ]); 67 | }); 68 | test('alphanumeric before', () { 69 | final result = parser.parse('ffoo '); 70 | expect(result, isA()); 71 | }); 72 | test('alphanumeric after', () { 73 | final result = parser.parse(' foof'); 74 | expect(result, isA()); 75 | }); 76 | }); 77 | }); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /test/org/grammar/radio_target_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('radio target', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.radioTarget()).end(); 9 | test('single character', () { 10 | final result = parser.parse('<<>>'); 11 | expect(result.value, ['<<<', '!', '>>>']); 12 | }); 13 | test('multiple words', () { 14 | final result = parser.parse('<<>>'); 15 | expect(result.value, ['<<<', 'foo bar', '>>>']); 16 | }); 17 | test('empty', () { 18 | final result = parser.parse('<<<>>>'); 19 | expect(result, isA()); 20 | }); 21 | test('whitespace only', () { 22 | final result = parser.parse('<<< >>>'); 23 | expect(result, isA()); 24 | }); 25 | test('contains forbidden character', () { 26 | final result = parser.parse('<< bar>>>'); 27 | expect(result, isA()); 28 | }); 29 | test('too few brackets', () { 30 | final result = parser.parse('<>'); 31 | expect(result, isA()); 32 | }); 33 | test('too many brackets', () { 34 | final result = parser.parse('<<<>>>'); 35 | expect(result, isA()); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/org/grammar/statistics_cookie_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('progress cookie', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.statsCookie()).end(); 9 | group('percentage', () { 10 | test('simple', () { 11 | final result = parser.parse('[50%]'); 12 | expect(result.value, ['[', '50', '%', ']']); 13 | }); 14 | test('empty', () { 15 | final result = parser.parse('[%]'); 16 | expect(result.value, ['[', '', '%', ']']); 17 | }); 18 | test('invalid', () { 19 | final result = parser.parse('[1/2/3]'); 20 | expect(result, isA()); 21 | }); 22 | }); 23 | group('fraction', () { 24 | test('simple', () { 25 | final result = parser.parse('[1/2]'); 26 | expect(result.value, ['[', '1', '/', '2', ']']); 27 | }); 28 | test('empty', () { 29 | final result = parser.parse('[/]'); 30 | expect(result.value, ['[', '', '/', '', ']']); 31 | }); 32 | test('partial', () { 33 | final result = parser.parse('[/2]'); 34 | expect(result.value, ['[', '', '/', '2', ']']); 35 | }); 36 | test('invalid', () { 37 | final result = parser.parse('[50%50]'); 38 | expect(result, isA()); 39 | }); 40 | }); 41 | group('invalid', () { 42 | test('empty', () { 43 | final result = parser.parse('[]'); 44 | expect(result, isA()); 45 | }); 46 | test('both delimiters', () { 47 | final result = parser.parse('[1/2%]'); 48 | expect(result, isA()); 49 | }); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/org/grammar/subscript_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('subscript', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = letter() & grammar.buildFrom(grammar.subscript()).end(); 9 | 10 | test('single number', () { 11 | final result = parser.parse('a_4'); 12 | expect(result.value, [ 13 | 'a', 14 | ['_', '4'], 15 | ]); 16 | }); 17 | test('single letter', () { 18 | final result = parser.parse('a_a'); 19 | expect(result.value, [ 20 | 'a', 21 | ['_', 'a'], 22 | ]); 23 | }); 24 | test('multiple alphanum', () { 25 | final result = parser.parse('a_a1b2'); 26 | expect(result.value, [ 27 | 'a', 28 | ['_', 'a1b2'], 29 | ]); 30 | }); 31 | test('multiple alphanum fail', () { 32 | final result = parser.parse('a_a1 b2'); 33 | expect(result, isA()); 34 | }); 35 | test('bracketed expression', () { 36 | final result = parser.parse('a_{a1 b2}'); 37 | expect(result.value, [ 38 | 'a', 39 | ['_', '{a1 b2}'], 40 | ]); 41 | }); 42 | test('nested bracketed expression', () { 43 | final result = parser.parse('a_{a1 {b2}}'); 44 | expect(result.value, [ 45 | 'a', 46 | ['_', '{a1 {b2}}'], 47 | ]); 48 | }); 49 | test('sexp', () { 50 | final result = parser.parse('a_(a1 b2)'); 51 | expect(result.value, [ 52 | 'a', 53 | ['_', '(a1 b2)'], 54 | ]); 55 | }); 56 | test('nested sexp', () { 57 | final result = parser.parse('a_(a1 (b2))'); 58 | expect(result.value, [ 59 | 'a', 60 | ['_', '(a1 (b2))'], 61 | ]); 62 | }); 63 | test('asterisk', () { 64 | final result = parser.parse('a_*'); 65 | expect(result.value, [ 66 | 'a', 67 | ['_', '*'], 68 | ]); 69 | }); 70 | test('numerical', () { 71 | final result = parser.parse('a_-1e24'); 72 | expect(result.value, [ 73 | 'a', 74 | ['_', '-1e24'], 75 | ]); 76 | }); 77 | test('non-ASCII', () { 78 | final result = parser.parse('a_あ'); 79 | expect(result.value, [ 80 | 'a', 81 | ['_', 'あ'], 82 | ]); 83 | }); 84 | test('edge case', () { 85 | final result = parser.parse('a_a..a'); 86 | expect(result.value, [ 87 | 'a', 88 | ['_', 'a..a'], 89 | ]); 90 | }); 91 | test('nested', () { 92 | final result = parser.parse('a_{a1_{b2}}'); 93 | expect( 94 | result.value, 95 | [ 96 | 'a', 97 | ['_', '{a1_{b2}}'] 98 | ], 99 | reason: 'body is parsed in separate phase'); 100 | }); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /test/org/grammar/superscript_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('superscript', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = letter() & grammar.buildFrom(grammar.superscript()).end(); 9 | 10 | test('single number', () { 11 | final result = parser.parse('a^4'); 12 | expect(result.value, [ 13 | 'a', 14 | ['^', '4'], 15 | ]); 16 | }); 17 | test('single letter', () { 18 | final result = parser.parse('a^a'); 19 | expect(result.value, [ 20 | 'a', 21 | ['^', 'a'], 22 | ]); 23 | }); 24 | test('multiple alphanum', () { 25 | final result = parser.parse('a^a1b2'); 26 | expect(result.value, [ 27 | 'a', 28 | ['^', 'a1b2'], 29 | ]); 30 | }); 31 | test('multiple alphanum fail', () { 32 | final result = parser.parse('a^a1 b2'); 33 | expect(result, isA()); 34 | }); 35 | test('bracketed expression', () { 36 | final result = parser.parse('a^{a1 b2}'); 37 | expect(result.value, [ 38 | 'a', 39 | ['^', '{a1 b2}'], 40 | ]); 41 | }); 42 | test('nested bracketed expression', () { 43 | final result = parser.parse('a^{a1 {b2}}'); 44 | expect(result.value, [ 45 | 'a', 46 | ['^', '{a1 {b2}}'], 47 | ]); 48 | }); 49 | test('sexp', () { 50 | final result = parser.parse('a^(a1 b2)'); 51 | expect(result.value, [ 52 | 'a', 53 | ['^', '(a1 b2)'], 54 | ]); 55 | }); 56 | test('nested sexp', () { 57 | final result = parser.parse('a^(a1 (b2))'); 58 | expect(result.value, [ 59 | 'a', 60 | ['^', '(a1 (b2))'], 61 | ]); 62 | }); 63 | test('asterisk', () { 64 | final result = parser.parse('a^*'); 65 | expect(result.value, [ 66 | 'a', 67 | ['^', '*'], 68 | ]); 69 | }); 70 | test('numerical', () { 71 | final result = parser.parse('a^-1e24'); 72 | expect(result.value, [ 73 | 'a', 74 | ['^', '-1e24'], 75 | ]); 76 | }); 77 | test('non-ASCII', () { 78 | final result = parser.parse('a^あ'); 79 | expect(result.value, [ 80 | 'a', 81 | ['^', 'あ'], 82 | ]); 83 | }); 84 | test('edge case', () { 85 | final result = parser.parse('a^a..a'); 86 | expect(result.value, [ 87 | 'a', 88 | ['^', 'a..a'], 89 | ]); 90 | }); 91 | test('nested', () { 92 | final result = parser.parse('a^{a1^{b2}}'); 93 | expect( 94 | result.value, 95 | [ 96 | 'a', 97 | ['^', '{a1^{b2}}'] 98 | ], 99 | reason: 'body is parsed in separate phase'); 100 | }); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /test/org/grammar/table_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | test('table', () { 7 | final grammar = OrgContentGrammarDefinition(); 8 | final parser = grammar.buildFrom(grammar.table()).end(); 9 | final result = parser.parse(''' | foo | bar | baz | 10 | |-----+-----+-----| 11 | | 1 | 2 | 3 | 12 | '''); 13 | expect(result.value, [ 14 | [ 15 | [ 16 | ' ', 17 | '|', 18 | [ 19 | [ 20 | ' ', 21 | ['foo'], 22 | ' |' 23 | ], 24 | [ 25 | ' ', 26 | ['bar'], 27 | ' |' 28 | ], 29 | [ 30 | ' ', 31 | ['baz'], 32 | ' |' 33 | ] 34 | ], 35 | '\n' 36 | ], 37 | [' ', '|-----+-----+-----|', '\n'], 38 | [ 39 | ' ', 40 | '|', 41 | [ 42 | [ 43 | ' ', 44 | ['1'], 45 | ' |' 46 | ], 47 | [ 48 | ' ', 49 | ['2'], 50 | ' |' 51 | ], 52 | [ 53 | ' ', 54 | ['3'], 55 | ' |' 56 | ] 57 | ], 58 | '\n' 59 | ] 60 | ], 61 | '' 62 | ]); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /test/org/model/arbitrary_greater_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final definition = OrgContentParserDefinition(); 7 | final parser = definition.buildFrom(definition.arbitraryGreaterBlock()).end(); 8 | test('arbitrary block', () { 9 | final markup = '''#+begin_blah 10 | foo ~bar~ 11 | bizbaz 12 | #+end_blah 13 | '''; 14 | final result = parser.parse(markup); 15 | final block = result.value as OrgBlock; 16 | expect(block.contains('bizbaz'), isTrue); 17 | expect(block.contains('あ'), isFalse); 18 | expect(block.toMarkup(), markup); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/org/model/block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('block', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.block()).end(); 9 | test('block', () { 10 | final markup = '''#+begin_example 11 | echo 'foo' 12 | rm bar 13 | #+end_example 14 | '''; 15 | final result = parser.parse(markup); 16 | final block = result.value as OrgBlock; 17 | expect(block.contains("echo 'foo'"), isTrue); 18 | expect(block.contains('あ'), isFalse); 19 | expect(block.toMarkup(), markup); 20 | }); 21 | group('source block', () { 22 | test('simple', () { 23 | final markup = '''#+begin_src sh 24 | echo 'foo' 25 | rm bar 26 | #+end_src 27 | '''; 28 | var result = parser.parse(markup); 29 | var block = result.value as OrgSrcBlock; 30 | expect(block.contains("echo 'foo'"), isTrue); 31 | expect(block.contains('あ'), isFalse); 32 | expect(block.toMarkup(), markup); 33 | }); 34 | test('empty', () { 35 | final markup = '''#+begin_src 36 | #+end_src'''; 37 | final result = parser.parse(markup); 38 | final block = result.value as OrgSrcBlock; 39 | expect(block.contains('あ'), isFalse); 40 | expect(block.toMarkup(), markup); 41 | }); 42 | }); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /test/org/model/citation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Citations', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.citation()).end(); 9 | test('simple', () { 10 | final markup = '[cite:@foo]'; 11 | final result = parser.parse(markup); 12 | final citation = result.value as OrgCitation; 13 | expect(citation.body, '@foo'); 14 | expect(citation.getKeys(), ['foo']); 15 | expect(citation.toMarkup(), markup); 16 | expect(citation.contains('foo'), isTrue); 17 | expect(citation.contains('あ'), isFalse); 18 | }); 19 | test('multiple keys', () { 20 | final markup = '[cite:@foo;@bar;@foo]'; 21 | final result = parser.parse(markup); 22 | final citation = result.value as OrgCitation; 23 | expect(citation.body, '@foo;@bar;@foo'); 24 | expect(citation.getKeys(), ['foo', 'bar', 'foo']); 25 | expect(citation.toMarkup(), markup); 26 | expect(citation.contains('foo'), isTrue); 27 | expect(citation.contains('あ'), isFalse); 28 | }); 29 | test('prefix and suffix', () { 30 | final markup = '[cite/style:pre;pre2@bar suff;suff2]'; 31 | final result = parser.parse(markup); 32 | final citation = result.value as OrgCitation; 33 | expect(citation.body, 'pre;pre2@bar suff;suff2'); 34 | expect(citation.getKeys(), ['bar']); 35 | expect(citation.toMarkup(), markup); 36 | expect(citation.contains('suff2'), isTrue); 37 | expect(citation.contains('あ'), isFalse); 38 | }); 39 | test('invalid', () { 40 | final markup = '[cite:foo]'; 41 | final result = parser.parse(markup); 42 | expect(result, isA()); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/org/model/comment_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Comments', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.comment()).end(); 9 | test('simple', () { 10 | final markup = '# foo bar'; 11 | final result = parser.parse(markup); 12 | final comment = result.value as OrgComment; 13 | expect(comment.contains('foo'), isTrue); 14 | expect(comment.contains('あ'), isFalse); 15 | expect(comment.toMarkup(), markup); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/org/model/drawer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('drawer', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.drawer()).end(); 9 | test('indented', () { 10 | final markup = ''' :foo: 11 | :bar: baz 12 | :bizz: buzz 13 | :end: 14 | 15 | '''; 16 | final result = parser.parse(markup); 17 | final drawer = result.value as OrgDrawer; 18 | expect(drawer.contains('foo'), isTrue); 19 | expect(drawer.contains('あ'), isFalse); 20 | expect(drawer.toMarkup(), markup); 21 | }); 22 | test('simple', () { 23 | final markup = ''':LOGBOOK: 24 | a 25 | :END: 26 | '''; 27 | final result = parser.parse(markup); 28 | final drawer = result.value as OrgDrawer; 29 | expect(drawer.contains('a'), isTrue); 30 | expect(drawer.contains('あ'), isFalse); 31 | expect(drawer.toMarkup(), markup); 32 | }); 33 | test('empty', () { 34 | final markup = ''':FOOBAR: 35 | :END:'''; 36 | final result = parser.parse(markup); 37 | final drawer = result.value as OrgDrawer; 38 | expect(drawer.contains('FOOBAR'), isTrue); 39 | expect(drawer.contains('あ'), isFalse); 40 | expect(drawer.toMarkup(), markup); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/org/model/dynamic_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('dynamic block', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.dynamicBlock()).end(); 9 | test('normal', () { 10 | final markup = '''#+BEGIN: myblock :parameter1 value1 :parameter2 value2 11 | foobar 12 | #+END: 13 | '''; 14 | final result = parser.parse(markup); 15 | final block = result.value as OrgDynamicBlock; 16 | expect(block.contains('myblock'), isTrue); 17 | expect(block.contains('foobar'), isTrue); 18 | expect(block.contains('あ'), isFalse); 19 | expect(block.toMarkup(), markup); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/org/model/entity_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('entity', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.entity()).end(); 9 | test('simple', () { 10 | final markup = r'\frac12'; 11 | final result = parser.parse(markup); 12 | final entity = result.value as OrgEntity; 13 | expect(entity.contains('frac12'), isTrue); 14 | expect(entity.contains('あ'), isFalse); 15 | expect(entity.toMarkup(), markup); 16 | }); 17 | test('with terminator', () { 18 | final markup = r'\foobar{}'; 19 | final result = parser.parse(markup); 20 | final entity = result.value as OrgEntity; 21 | expect(entity.contains('foobar'), isTrue); 22 | expect(entity.contains('あ'), isFalse); 23 | expect(entity.toMarkup(), markup); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/org/model/fixed_width_area_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('fixed-width area', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.fixedWidthArea()).end(); 9 | test('multiline', () { 10 | final markup = ''': foo 11 | : bar 12 | '''; 13 | var result = parser.parse(markup); 14 | var area = result.value as OrgFixedWidthArea; 15 | expect(area.contains('foo'), isTrue); 16 | expect(area.contains('あ'), isFalse); 17 | expect(area.toMarkup(), markup); 18 | }); 19 | test('empty', () { 20 | final markup = ': '; 21 | final result = parser.parse(markup); 22 | final area = result.value as OrgFixedWidthArea; 23 | expect(area.contains(':'), isTrue); 24 | expect(area.contains('あ'), isFalse); 25 | expect(area.toMarkup(), markup); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/org/model/footnote_reference_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('footnote reference', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.footnoteReference()).end(); 9 | test('simple', () { 10 | final markup = '[fn:1]'; 11 | var result = parser.parse(markup); 12 | final named = result.value as OrgFootnoteReference; 13 | expect(named.contains('1'), isTrue); 14 | expect(named.contains('あ'), isFalse); 15 | expect(named.toMarkup(), markup); 16 | }); 17 | test('with definition', () { 18 | final markup = '[fn:: who /what/ why]'; 19 | final result = parser.parse(markup); 20 | final anonymous = result.value as OrgFootnoteReference; 21 | expect(anonymous.contains('who'), isTrue); 22 | expect(anonymous.contains('あ'), isFalse); 23 | expect(anonymous.toMarkup(), markup); 24 | }); 25 | test('with name', () { 26 | final markup = '[fn:abc123: when /where/ how]'; 27 | final result = parser.parse(markup); 28 | final inline = result.value as OrgFootnoteReference; 29 | expect(inline.contains('abc123'), isTrue); 30 | expect(inline.contains('あ'), isFalse); 31 | expect(inline.toMarkup(), markup); 32 | }); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /test/org/model/footnote_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('footnote', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.footnote()).end(); 9 | test('simple', () { 10 | final markup = '[fn:1] foo *bar* biz baz'; 11 | var result = parser.parse(markup); 12 | final footnote = result.value as OrgFootnote; 13 | expect(footnote.contains('foo'), isTrue); 14 | expect(footnote.contains('あ'), isFalse); 15 | expect(footnote.toMarkup(), markup); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/org/model/greater_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final definition = OrgContentParserDefinition(); 7 | final parser = definition.buildFrom(definition.greaterBlock()).end(); 8 | test('greater block', () { 9 | final markup = '''#+begin_center 10 | foo ~bar~ 11 | bizbaz 12 | #+end_center 13 | '''; 14 | final result = parser.parse(markup); 15 | final block = result.value as OrgBlock; 16 | expect(block.contains('bizbaz'), isTrue); 17 | expect(block.contains('foo ~bar~'), false); 18 | expect(block.toMarkup(), markup); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/org/model/horizontal_rule_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('horizontal rule', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.horizontalRule()).end(); 9 | test('minimal', () { 10 | final markup = '-----'; 11 | final result = parser.parse(markup); 12 | final rule = result.value as OrgHorizontalRule; 13 | expect(rule.toMarkup(), markup); 14 | expect(rule.contains('-----'), isTrue); 15 | expect(rule.contains('あ'), isFalse); 16 | }); 17 | test('trailing', () { 18 | final markup = '''-----${' '} 19 | '''; 20 | final result = parser.parse(markup); 21 | final rule = result.value as OrgHorizontalRule; 22 | expect(rule.toMarkup(), markup); 23 | expect(rule.contains(' '), isTrue); 24 | expect(rule.contains('あ'), isFalse); 25 | }); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/org/model/inline_src_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('inline src', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.inlineSourceBlock()).end(); 9 | test('no args', () { 10 | final markup = '''src_sh{echo "foo"}'''; 11 | final result = parser.parse(markup); 12 | final block = result.value as OrgInlineSrcBlock; 13 | expect(block.contains('echo "foo"'), isTrue); 14 | expect(block.contains('sh'), isTrue); 15 | expect(block.contains('あ'), isFalse); 16 | expect(block.toMarkup(), markup); 17 | }); 18 | test('with args', () { 19 | final markup = '''src_ruby[:exports code]{println "foo"}'''; 20 | final result = parser.parse(markup); 21 | final block = result.value as OrgInlineSrcBlock; 22 | expect(block.contains('println "foo"'), isTrue); 23 | expect(block.contains('ruby'), isTrue); 24 | expect(block.contains(':exports'), isTrue); 25 | expect(block.contains('あ'), isFalse); 26 | expect(block.toMarkup(), markup); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /test/org/model/latex_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final definition = OrgContentParserDefinition(); 7 | final parser = definition.buildFrom(definition.latexBlock()).end(); 8 | test('LaTeX block', () { 9 | final markup = r'''\begin{equation} 10 | \begin{matrix} 11 | a & b \\ 12 | c & d 13 | \end{matrix} 14 | \end{equation} 15 | '''; 16 | final result = parser.parse(markup); 17 | final latex = result.value as OrgLatexBlock; 18 | expect(latex.contains(r'\begin{matrix}'), isTrue); 19 | expect(latex.contains(r'あ'), isFalse); 20 | expect(latex.toMarkup(), markup); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/org/model/latex_inline_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('inline LaTeX', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.latexInline()).end(); 9 | test(r'single-$ delimiter', () { 10 | final markup = r'$i$'; 11 | final result = parser.parse(markup); 12 | final latex = result.value as OrgLatexInline; 13 | expect(latex.contains('i'), isTrue); 14 | expect(latex.contains('あ'), isFalse); 15 | expect(latex.toMarkup(), markup); 16 | }); 17 | test(r'double-$ delimiter', () { 18 | final markup = r'$$ a^2 $$'; 19 | final result = parser.parse(markup); 20 | final latex = result.value as OrgLatexInline; 21 | expect(latex.contains('a^2'), isTrue); 22 | expect(latex.contains('あ'), isFalse); 23 | expect(latex.toMarkup(), markup); 24 | }); 25 | test('paren delimiter', () { 26 | final markup = r'\( foo \)'; 27 | final result = parser.parse(markup); 28 | final latex = result.value as OrgLatexInline; 29 | expect(latex.contains('foo'), isTrue); 30 | expect(latex.contains('あ'), isFalse); 31 | expect(latex.toMarkup(), markup); 32 | }); 33 | test('bracket delimiter', () { 34 | final markup = r'\[ bar \]'; 35 | final result = parser.parse(markup); 36 | final latex = result.value as OrgLatexInline; 37 | expect(latex.contains('bar'), isTrue); 38 | expect(latex.contains('あ'), isFalse); 39 | expect(latex.toMarkup(), markup); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/org/model/link_target_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('link target', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.linkTarget()).end(); 9 | test('single character', () { 10 | final markup = '<>'; 11 | final target = parser.parse(markup).value as OrgLinkTarget; 12 | expect(target.contains('!'), isTrue); 13 | expect(target.contains('あ'), isFalse); 14 | expect(target.toMarkup(), markup); 15 | }); 16 | test('multiple workds', () { 17 | final markup = '<>'; 18 | final target = parser.parse(markup).value as OrgLinkTarget; 19 | expect(target.contains('foo'), isTrue); 20 | expect(target.contains('あ'), isFalse); 21 | expect(target.toMarkup(), markup); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/org/model/link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('link', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.link()).end(); 9 | test('brackets in location', () { 10 | final markup = '[[*\\[wtf\\] what?][[lots][of][boxes]\u200b]]'; 11 | final result = parser.parse(markup); 12 | final link = result.value as OrgBracketLink; 13 | expect(link.contains('what?'), isTrue); 14 | expect(link.contains('あ'), isFalse); 15 | expect(link.toMarkup(), markup); 16 | }); 17 | test('link with search option', () { 18 | final markup = '[[foo::1][bar]]'; 19 | final result = parser.parse(markup); 20 | final link = result.value as OrgBracketLink; 21 | expect(link.contains('foo'), isTrue); 22 | expect(link.contains('あ'), isFalse); 23 | expect(link.toMarkup(), markup); 24 | }); 25 | test('quotes in search option', () { 26 | final markup = r'[[foo::"\[1\]"][bar]]'; 27 | final result = parser.parse(markup); 28 | final link = result.value as OrgBracketLink; 29 | expect(link.contains('foo'), isTrue); 30 | expect(link.contains('あ'), isFalse); 31 | expect(link.toMarkup(), markup); 32 | }); 33 | test('no description', () { 34 | final markup = '[[foo::1]]'; 35 | final result = parser.parse(markup); 36 | final link = result.value as OrgBracketLink; 37 | expect(link.contains('foo'), isTrue); 38 | expect(link.contains('あ'), isFalse); 39 | expect(link.toMarkup(), markup); 40 | }); 41 | test('plain link', () { 42 | final markup = 'http://example.com'; 43 | final result = parser.parse(markup); 44 | final link = result.value as OrgLink; 45 | expect(link.contains('example'), isTrue); 46 | expect(link.contains('あ'), isFalse); 47 | expect(link.toMarkup(), markup); 48 | }); 49 | test('nested markup', () { 50 | final markup = '[[foo][*bar*]]'; 51 | final result = parser.parse(markup); 52 | final link = result.value as OrgBracketLink; 53 | final nested = link.find((node) => true); 54 | expect(nested, isNotNull); 55 | expect(nested!.node.toMarkup(), '*bar*'); 56 | expect(link.toMarkup(), markup); 57 | }); 58 | test('nested link', () { 59 | final markup = '[[foo][link with [[https://orgro.org][link]​] inside]]'; 60 | final result = parser.parse(markup); 61 | final link = result.value as OrgBracketLink; 62 | final nested = 63 | link.find((node) => !identical(node, link)); 64 | expect(nested, isNull); 65 | expect( 66 | link.description!.children.every((node) => node is OrgPlainText), 67 | isTrue, 68 | ); 69 | expect(link.toMarkup(), markup); 70 | }); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /test/org/model/local_variables_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('local variables', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.localVariables()).end(); 9 | test('simple', () { 10 | final markup = '''# Local Variables: 11 | # foo: bar 12 | # End: '''; 13 | final result = parser.parse(markup); 14 | final lvars = result.value as OrgLocalVariables; 15 | expect(lvars.contains('foo'), isTrue); 16 | expect(lvars.contains('あ'), isFalse); 17 | expect(lvars.toMarkup(), markup); 18 | }); 19 | test('with suffix', () { 20 | final markup = '''# Local Variables: # 21 | # foo: bar # 22 | # End: #'''; 23 | final result = parser.parse(markup); 24 | final lvars = result.value as OrgLocalVariables; 25 | expect(lvars.contains('foo'), isTrue); 26 | expect(lvars.contains('あ'), isFalse); 27 | expect(lvars.toMarkup(), markup); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/org/model/macro_reference_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('macro reference', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.macroReference()).end(); 9 | test('with args', () { 10 | final markup = '{{{name(arg1, arg2)}}}'; 11 | var result = parser.parse(markup); 12 | var ref = result.value as OrgMacroReference; 13 | expect(ref.contains('name'), isTrue); 14 | expect(ref.contains('あ'), isFalse); 15 | expect(ref.toMarkup(), markup); 16 | }); 17 | test('simple', () { 18 | final markup = '{{{foobar}}}'; 19 | final result = parser.parse(markup); 20 | final ref = result.value as OrgMacroReference; 21 | expect(ref.contains('foobar'), isTrue); 22 | expect(ref.contains('あ'), isFalse); 23 | expect(ref.toMarkup(), markup); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/org/model/markups_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('markup', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.markups()).end(); 9 | test('with line break', () { 10 | final markup = '''/foo 11 | bar/'''; 12 | var result = parser.parse(markup); 13 | final markupNode = result.value as OrgMarkup; 14 | expect(markupNode.contains('foo'), isTrue); 15 | expect(markupNode.contains('あ'), isFalse); 16 | expect(markupNode.toMarkup(), markup); 17 | }); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/org/model/pgp_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('PGP block', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.pgpBlock()).end(); 9 | test('simple', () { 10 | final markup = '''-----BEGIN PGP MESSAGE----- 11 | 12 | jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O 13 | X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY= 14 | =chda 15 | -----END PGP MESSAGE----- 16 | '''; 17 | final result = parser.parse(markup); 18 | final pgp = result.value as OrgPgpBlock; 19 | expect(pgp.contains('BEGIN PGP MESSAGE'), isTrue); 20 | expect(pgp.contains('あ'), isFalse); 21 | expect(pgp.toMarkup(), markup); 22 | expect(pgp.toRfc4880(), markup.trim()); 23 | }); 24 | test('indented', () { 25 | final markup = ''' -----BEGIN PGP MESSAGE----- 26 | 27 | jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O 28 | X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY= 29 | =chda 30 | -----END PGP MESSAGE----- 31 | '''; 32 | final result = parser.parse(markup); 33 | final pgp = result.value as OrgPgpBlock; 34 | expect(pgp.contains('END PGP MESSAGE'), isTrue); 35 | expect(pgp.contains('あ'), isFalse); 36 | expect(pgp.toMarkup(), markup); 37 | expect(pgp.toRfc4880(), '''-----BEGIN PGP MESSAGE----- 38 | 39 | jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O 40 | X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY= 41 | =chda 42 | -----END PGP MESSAGE-----'''); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/org/model/planning_entry_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final definition = OrgContentParserDefinition(); 7 | final parser = definition.buildFrom(definition.planningEntry()).end(); 8 | test('planning entry', () { 9 | final markup = 'CLOCK: [2021-01-23 Sat 09:30]--[2021-01-23 Sat 10:19]'; 10 | final result = parser.parse(markup); 11 | final planningLine = result.value as OrgPlanningEntry; 12 | expect(planningLine.contains('CLOCK'), isTrue); 13 | expect(planningLine.contains('あ'), isFalse); 14 | expect(planningLine.toMarkup(), markup); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/org/model/radio_link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('radio link', () { 7 | final definition = OrgContentParserDefinition(radioTargets: ['foo', 'bar']); 8 | final parser = definition.buildFrom(definition.radioLink()).end(); 9 | test('simple', () { 10 | final markup = 'foo'; 11 | final result = parser.parse(markup); 12 | final target = result.value as OrgRadioLink; 13 | expect(target.contains('foo'), isTrue); 14 | expect(target.contains('あ'), isFalse); 15 | expect(target.toMarkup(), markup); 16 | }); 17 | test('found in content', () { 18 | final doc = OrgDocument.parse(''' 19 | <<>> 20 | foo''', interpretEmbeddedSettings: true); 21 | final radioLink = doc.find((_) => true); 22 | expect(radioLink!.node.content, 'foo'); 23 | }); 24 | test('found in headline', () { 25 | final doc = OrgDocument.parse(''' 26 | * foo 27 | <<>>''', interpretEmbeddedSettings: true); 28 | final radioLink = doc.find((_) => true); 29 | expect(radioLink!.node.content, 'foo'); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/org/model/radio_target_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('radio target', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.radioTarget()).end(); 9 | test('single character', () { 10 | final markup = '<<>>'; 11 | final target = parser.parse(markup).value as OrgRadioTarget; 12 | expect(target.contains('!'), isTrue); 13 | expect(target.contains('あ'), isFalse); 14 | expect(target.toMarkup(), markup); 15 | }); 16 | test('multiple workds', () { 17 | final markup = '<<>>'; 18 | final target = parser.parse(markup).value as OrgRadioTarget; 19 | expect(target.contains('foo'), isTrue); 20 | expect(target.contains('あ'), isFalse); 21 | expect(target.toMarkup(), markup); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/org/model/subscript_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('subscript', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = 9 | seq2(letter(), definition.buildFrom(definition.subscript())).end(); 10 | test('with entity', () { 11 | final result = parser.parse(r'a_\alpha'); 12 | final (_, OrgSubscript sup) = result.value; 13 | expect(sup.contains('alpha'), isTrue); 14 | expect(sup.contains(r'\alpha'), isFalse); 15 | expect(sup.toMarkup(), r'_\alpha'); 16 | }); 17 | test('nested bracketed expression', () { 18 | final result = parser.parse('a_{a1 {b2}}'); 19 | final (_, OrgSubscript sup) = result.value; 20 | expect(sup.contains('b2'), isTrue); 21 | expect(sup.contains('あ'), isFalse); 22 | expect(sup.toMarkup(), '_{a1 {b2}}'); 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /test/org/model/superscript_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('superscript', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = 9 | seq2(letter(), definition.buildFrom(definition.superscript())).end(); 10 | test('nested bracketed expression', () { 11 | final result = parser.parse('a^{a1 {b2}}'); 12 | final (_, OrgSuperscript sup) = result.value; 13 | expect(sup.contains('b2'), isTrue); 14 | expect(sup.contains('あ'), isFalse); 15 | expect(sup.toMarkup(), '^{a1 {b2}}'); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /test/org/model/table_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('table', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.table()).end(); 9 | test('simple', () { 10 | final markup = ''' | foo | *bar* | baz | 11 | |-----+-----+-----| 12 | | 1 | 2 | 3 | 13 | '''; 14 | final result = parser.parse(markup); 15 | final table = result.value as OrgTable; 16 | expect(table.contains('foo'), isTrue); 17 | expect(table.contains('bar'), isTrue); 18 | expect(table.contains('*bar*'), isFalse); 19 | expect(table.toMarkup(), markup); 20 | }); 21 | test('empty', () { 22 | final markup = '||'; 23 | final result = parser.parse(markup); 24 | final table = result.value as OrgTable; 25 | expect(table.toMarkup(), markup); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/org/parser/block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('block', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.block()).end(); 9 | test('example block', () { 10 | final result = parser.parse('''#+begin_example 11 | echo 'foo' 12 | rm bar 13 | #+end_example 14 | '''); 15 | final block = result.value as OrgBlock; 16 | final body = block.body as OrgPlainText; 17 | expect(block.header, '#+begin_example\n'); 18 | expect(body.content, ' echo \'foo\'\n rm bar\n'); 19 | expect(block.footer, '#+end_example'); 20 | expect(block.trailing, '\n'); 21 | expect(block.type, 'example'); 22 | }); 23 | group('source block', () { 24 | test('simple', () { 25 | final result = parser.parse('''#+begin_src sh 26 | echo 'foo' 27 | rm bar 28 | #+end_src 29 | '''); 30 | final block = result.value as OrgSrcBlock; 31 | final body = block.body as OrgPlainText; 32 | expect(block.language, 'sh'); 33 | expect(block.header, '#+begin_src sh\n'); 34 | expect(body.content, ' echo \'foo\'\n rm bar\n'); 35 | expect(block.footer, '#+end_src'); 36 | expect(block.trailing, '\n'); 37 | expect(block.type, 'src'); 38 | }); 39 | test('empty', () { 40 | final result = parser.parse('''#+begin_src 41 | #+end_src'''); 42 | final block = result.value as OrgSrcBlock; 43 | final body = block.body as OrgPlainText; 44 | expect(block.language, isNull); 45 | expect(body.content, ''); 46 | expect(block.type, 'src'); 47 | }); 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/org/parser/citation_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('citation', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.citation()).end(); 9 | test('simple', () { 10 | final result = parser.parse('[cite:@foo]'); 11 | final citation = result.value as OrgCitation; 12 | expect(citation.leading, '[cite'); 13 | expect(citation.style, isNull); 14 | expect(citation.delimiter, ':'); 15 | expect(citation.body, '@foo'); 16 | expect(citation.trailing, ']'); 17 | }); 18 | test('with style', () { 19 | final result = parser.parse('[cite/mystyle:@bar]'); 20 | final citation = result.value as OrgCitation; 21 | expect(citation.leading, '[cite'); 22 | expect(citation.style?.leading, '/'); 23 | expect(citation.style?.value, 'mystyle'); 24 | expect(citation.delimiter, ':'); 25 | expect(citation.body, '@bar'); 26 | expect(citation.trailing, ']'); 27 | }); 28 | test('multiple keys', () { 29 | final result = parser.parse('[cite:@foo;@bar]'); 30 | final citation = result.value as OrgCitation; 31 | expect(citation.leading, '[cite'); 32 | expect(citation.style, isNull); 33 | expect(citation.delimiter, ':'); 34 | expect(citation.body, '@foo;@bar'); 35 | expect(citation.trailing, ']'); 36 | }); 37 | test('prefix and suffix', () { 38 | final result = parser.parse('[cite:a ;b @foo c; d]'); 39 | final citation = result.value as OrgCitation; 40 | expect(citation.leading, '[cite'); 41 | expect(citation.style, isNull); 42 | expect(citation.delimiter, ':'); 43 | expect(citation.body, 'a ;b @foo c; d'); 44 | expect(citation.trailing, ']'); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/org/parser/comment_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('Comment', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.comment()).end(); 9 | test('simple', () { 10 | final result = parser.parse('# foo bar'); 11 | final comment = result.value as OrgComment; 12 | expect(comment.indent, ''); 13 | expect(comment.start, '# '); 14 | expect(comment.content, 'foo bar'); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test/org/parser/drawer_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('drawer', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.drawer()).end(); 9 | test('indented', () { 10 | final result = parser.parse(''' :foo: 11 | :bar: baz 12 | :bizz: buzz 13 | :end: 14 | 15 | '''); 16 | final drawer = result.value as OrgDrawer; 17 | expect(drawer.header, ':foo:\n'); 18 | expect(drawer.properties().length, 2); 19 | final body = drawer.body as OrgContent; 20 | final property = body.children[0] as OrgProperty; 21 | expect(property.key, ':bar:'); 22 | final value = property.value.children[0] as OrgPlainText; 23 | expect(value.content, ' baz'); 24 | expect(drawer.footer, ' :end:'); 25 | expect(drawer.properties().first, property); 26 | }); 27 | test('simple', () { 28 | final result = parser.parse(''':LOGBOOK: 29 | a 30 | :END: 31 | '''); 32 | final drawer = result.value as OrgDrawer; 33 | expect(drawer.header, ':LOGBOOK:\n'); 34 | expect(drawer.properties().isEmpty, isTrue); 35 | final body = drawer.body as OrgContent; 36 | final text = body.children[0] as OrgPlainText; 37 | expect(text.content, 'a\n'); 38 | }); 39 | test('rich property', () { 40 | final result = parser.parse(''':LOGBOOK: 41 | :foo: *bar* 42 | :END: 43 | '''); 44 | final drawer = result.value as OrgDrawer; 45 | expect(drawer.header, ':LOGBOOK:\n'); 46 | final property = drawer.properties().first; 47 | final value = property.value.children[1] as OrgMarkup; 48 | expect(value.toMarkup(), '*bar*'); 49 | }); 50 | test('empty', () { 51 | final result = parser.parse(''':FOOBAR: 52 | :END:'''); 53 | final drawer = result.value as OrgDrawer; 54 | expect(drawer.properties().isEmpty, isTrue); 55 | final body = drawer.body as OrgContent; 56 | expect(body.children.isEmpty, isTrue); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/org/parser/dynamic_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('dynamic block', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.dynamicBlock()).end(); 9 | test('normal', () { 10 | final result = 11 | parser.parse('''#+BEGIN: myblock :parameter1 value1 :parameter2 value2 12 | foobar 13 | #+END: 14 | '''); 15 | final block = result.value as OrgDynamicBlock; 16 | expect(block.header, 17 | '#+BEGIN: myblock :parameter1 value1 :parameter2 value2\n'); 18 | expect(block.body.children[0].toMarkup(), ' foobar\n'); 19 | expect(block.footer, '#+END:'); 20 | expect(block.trailing, '\n'); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/org/parser/entity_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('entity', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.entity()).end(); 9 | test('simple', () { 10 | final result = parser.parse(r'\frac12'); 11 | final entity = result.value as OrgEntity; 12 | expect(entity.leading, r'\'); 13 | expect(entity.name, r'frac12'); 14 | expect(entity.trailing, ''); 15 | }); 16 | test('with terminator', () { 17 | final result = parser.parse(r'\foobar{}'); 18 | final entity = result.value as OrgEntity; 19 | expect(entity.leading, r'\'); 20 | expect(entity.name, r'foobar'); 21 | expect(entity.trailing, '{}'); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/org/parser/fixed_width_area_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('fixed-width area', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.fixedWidthArea()).end(); 9 | test('multiline', () { 10 | final result = parser.parse(''': foo 11 | : bar 12 | '''); 13 | final area = result.value as OrgFixedWidthArea; 14 | expect(area.content, ''': foo 15 | : bar 16 | '''); 17 | }); 18 | test('empty', () { 19 | final result = parser.parse(': '); 20 | final area = result.value as OrgFixedWidthArea; 21 | expect(area.content, ': '); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/org/parser/footnote_reference_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('footnote reference', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.footnoteReference()).end(); 9 | test('simple', () { 10 | var result = parser.parse('[fn:1]'); 11 | final named = result.value as OrgFootnoteReference; 12 | expect(named.isDefinition, isFalse); 13 | expect(named.leading, '[fn:'); 14 | expect(named.name, '1'); 15 | expect(named.definition?.delimiter, isNull); 16 | expect(named.definition, isNull); 17 | expect(named.trailing, ']'); 18 | }); 19 | test('with definition', () { 20 | final result = parser.parse('[fn:: who /what/ why]'); 21 | final anonymous = result.value as OrgFootnoteReference; 22 | expect(anonymous.isDefinition, isFalse); 23 | expect(anonymous.leading, '[fn:'); 24 | expect(anonymous.name, isNull); 25 | expect(anonymous.definition?.delimiter, ':'); 26 | final defText0 = anonymous.definition!.value.children[0] as OrgPlainText; 27 | expect(defText0.content, ' who '); 28 | expect(anonymous.trailing, ']'); 29 | }); 30 | test('with name', () { 31 | final result = parser.parse('[fn:abc123: when /where/ how]'); 32 | final inline = result.value as OrgFootnoteReference; 33 | expect(inline.isDefinition, isFalse); 34 | expect(inline.leading, '[fn:'); 35 | expect(inline.name, 'abc123'); 36 | expect(inline.definition?.delimiter, ':'); 37 | final defText0 = inline.definition!.value.children[0] as OrgPlainText; 38 | expect(defText0.content, ' when '); 39 | expect(inline.trailing, ']'); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/org/parser/footnote_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('footnote', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.footnote()).end(); 9 | test('simple', () { 10 | final result = parser.parse('[fn:1] foo *bar* biz baz'); 11 | final footnote = result.value as OrgFootnote; 12 | expect(footnote.marker.isDefinition, isTrue); 13 | expect(footnote.marker.name, '1'); 14 | final firstText = footnote.content.children[0] as OrgPlainText; 15 | expect(firstText.content, ' foo '); 16 | }); 17 | test('invalid indent', () { 18 | final result = parser.parse(' [fn:2] bazinga'); 19 | expect(result, isA(), reason: 'Indent not allowed'); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/org/parser/greater_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final definition = OrgContentParserDefinition(); 7 | final parser = definition.buildFrom(definition.greaterBlock()).end(); 8 | test('greater block', () { 9 | final result = parser.parse('''#+begin_center 10 | foo ~bar~ 11 | bizbaz 12 | #+end_center 13 | '''); 14 | final block = result.value as OrgBlock; 15 | expect(block.header, '#+begin_center\n'); 16 | final body = block.body as OrgContent; 17 | final child1 = body.children[0] as OrgParagraph; 18 | expect(child1.indent, ' '); 19 | final gchild1 = child1.body.children.first as OrgPlainText; 20 | expect(gchild1.content, 'foo '); 21 | final gchild2 = child1.body.children[1] as OrgMarkup; 22 | final ggchild1Body = gchild2.content.children.first as OrgPlainText; 23 | expect(ggchild1Body.content, 'bar'); 24 | expect(block.footer, '#+end_center'); 25 | expect(block.trailing, '\n'); 26 | expect(block.type, 'center'); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/org/parser/headline_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('headline', () { 7 | final definition = OrgParserDefinition(); 8 | final parser = definition.buildFrom(definition.headline()).end(); 9 | test('full', () { 10 | final result = parser.parse('** TODO [#A] Title *foo* bar :biz:baz:'); 11 | final headline = result.value as OrgHeadline; 12 | final title = headline.title!.children[0] as OrgPlainText; 13 | expect(title.content, 'Title '); 14 | final titleEmphasis = headline.title!.children[1] as OrgMarkup; 15 | final titleEmphasisContent = 16 | titleEmphasis.content.children.single as OrgPlainText; 17 | expect(titleEmphasisContent.content, 'foo'); 18 | expect(headline.tags?.values, ['biz', 'baz']); 19 | }); 20 | test('empty', () { 21 | final result = parser.parse('* '); 22 | final headline = result.value as OrgHeadline; 23 | expect(headline.title, isNull); 24 | }); 25 | test('with latex', () { 26 | final result = parser.parse(r'* foo \( \pi \)'); 27 | final headline = result.value as OrgHeadline; 28 | final [title0, title1] = headline.title!.children; 29 | expect((title0 as OrgPlainText).content, 'foo '); 30 | expect((title1 as OrgLatexInline).content, r' \pi '); 31 | }); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/org/parser/horizontal_rule_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('horizontal rule', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.horizontalRule()).end(); 9 | test('minimal', () { 10 | final result = parser.parse('-----'); 11 | final rule = result.value as OrgHorizontalRule; 12 | expect(rule.indent, ''); 13 | expect(rule.content, '-----'); 14 | expect(rule.trailing, ''); 15 | }); 16 | test('indented', () { 17 | final result = parser.parse(' -----'); 18 | final rule = result.value as OrgHorizontalRule; 19 | expect(rule.indent, ' '); 20 | expect(rule.content, '-----'); 21 | expect(rule.trailing, ''); 22 | }); 23 | test('trailing', () { 24 | final result = parser.parse('''-----${' '} 25 | '''); 26 | final rule = result.value as OrgHorizontalRule; 27 | expect(rule.indent, ''); 28 | expect(rule.content, '-----'); 29 | expect(rule.trailing, ' \n'); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/org/parser/inline_src_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('inline src', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.inlineSourceBlock()).end(); 9 | test('no args', () { 10 | final result = parser.parse('''src_sh{echo "foo"}'''); 11 | final block = result.value as OrgInlineSrcBlock; 12 | expect(block.leading, 'src_'); 13 | expect(block.language, 'sh'); 14 | expect(block.arguments, isNull); 15 | expect(block.body, '{echo "foo"}'); 16 | }); 17 | test('with args', () { 18 | final result = parser.parse('''src_ruby[:exports code]{println "foo"}'''); 19 | final block = result.value as OrgInlineSrcBlock; 20 | expect(block.leading, 'src_'); 21 | expect(block.language, 'ruby'); 22 | expect(block.arguments, '[:exports code]'); 23 | expect(block.body, '{println "foo"}'); 24 | }); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /test/org/parser/latex_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final definition = OrgContentParserDefinition(); 7 | final parser = definition.buildFrom(definition.latexBlock()).end(); 8 | test('LaTeX block', () { 9 | final result = parser.parse(r'''\begin{equation} 10 | \begin{matrix} 11 | a & b \\ 12 | c & d 13 | \end{matrix} 14 | \end{equation} 15 | '''); 16 | final latex = result.value as OrgLatexBlock; 17 | expect(latex.environment, 'equation'); 18 | expect(latex.begin, r'\begin{equation}'); 19 | expect(latex.content, 20 | '\n\\begin{matrix}\n a & b \\\\\n c & d\n\\end{matrix}\n'); 21 | expect(latex.end, r'\end{equation}'); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/org/parser/latex_inline_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('inline LaTeX', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.latexInline()).end(); 9 | test(r'single-$ delimiter', () { 10 | final result = parser.parse(r'$i$'); 11 | final latex = result.value as OrgLatexInline; 12 | expect(latex.leadingDecoration, r'$'); 13 | expect(latex.content, r'i'); 14 | expect(latex.trailingDecoration, r'$'); 15 | }); 16 | test(r'double-$ delimiter', () { 17 | final result = parser.parse(r'$$ a^2 $$'); 18 | final latex = result.value as OrgLatexInline; 19 | expect(latex.leadingDecoration, r'$$'); 20 | expect(latex.content, r' a^2 '); 21 | expect(latex.trailingDecoration, r'$$'); 22 | }); 23 | test('paren delimiter', () { 24 | final result = parser.parse(r'\( foo \)'); 25 | final latex = result.value as OrgLatexInline; 26 | expect(latex.leadingDecoration, r'\('); 27 | expect(latex.content, r' foo '); 28 | expect(latex.trailingDecoration, r'\)'); 29 | }); 30 | test('bracket delimiter', () { 31 | final result = parser.parse(r'\[ bar \]'); 32 | final latex = result.value as OrgLatexInline; 33 | expect(latex.leadingDecoration, r'\['); 34 | expect(latex.content, r' bar '); 35 | expect(latex.trailingDecoration, r'\]'); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/org/parser/link_target_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('link target', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.linkTarget()).end(); 9 | test('single character', () { 10 | final result = parser.parse('<>'); 11 | final target = result.value as OrgLinkTarget; 12 | expect(target.leading, '<<'); 13 | expect(target.body, '!'); 14 | expect(target.trailing, '>>'); 15 | }); 16 | test('multiple words', () { 17 | final result = parser.parse('<>'); 18 | final target = result.value as OrgLinkTarget; 19 | expect(target.leading, '<<'); 20 | expect(target.body, 'foo bar'); 21 | expect(target.trailing, '>>'); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/org/parser/link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('link', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.link()).end(); 9 | test('brackets in location', () { 10 | final result = 11 | parser.parse('[[*\\[wtf\\] what?][[lots][of][boxes]\u200b]]'); 12 | final link = result.value as OrgBracketLink; 13 | expect(link.location, '*[wtf] what?'); 14 | expect(link.description!.children.single.toMarkup(), '[lots][of][boxes]'); 15 | }); 16 | test('link with search option', () { 17 | final result = parser.parse('[[foo::1][bar]]'); 18 | final link = result.value as OrgBracketLink; 19 | expect(link.description!.children.single.toMarkup(), 'bar'); 20 | expect(link.location, 'foo::1'); 21 | }); 22 | test('quotes in search option', () { 23 | final result = parser.parse(r'[[foo::"\[1\]"][bar]]'); 24 | final link = result.value as OrgBracketLink; 25 | expect(link.description!.children.single.toMarkup(), 'bar'); 26 | expect(link.location, 'foo::"[1]"'); 27 | }); 28 | test('no description', () { 29 | final result = parser.parse('[[foo::1]]'); 30 | final link = result.value as OrgBracketLink; 31 | expect(link.description, isNull); 32 | expect(link.location, 'foo::1'); 33 | }); 34 | test('plain link', () { 35 | final result = parser.parse('http://example.com'); 36 | final link = result.value as OrgLink; 37 | expect(link.location, 'http://example.com'); 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/org/parser/local_variables_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('local variables', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.localVariables()).end(); 9 | test('simple', () { 10 | final result = parser.parse('''# Local Variables: 11 | # foo: bar 12 | # End: '''); 13 | final variables = result.value as OrgLocalVariables; 14 | expect(variables.start, '# Local Variables:\n'); 15 | expect(variables.end, '# End: '); 16 | expect(variables.trailing, ''); 17 | expect(variables.entries.length, 1); 18 | expect( 19 | variables.entries[0], 20 | (prefix: '# ', content: 'foo: bar', suffix: '\n'), 21 | ); 22 | }); 23 | test('with indent and suffix', () { 24 | final result = parser.parse(''' /* Local Variables: */ 25 | /* foo: bar */ 26 | /* baz: bazinga */ 27 | /* End: */ 28 | 29 | '''); 30 | final variables = result.value as OrgLocalVariables; 31 | expect(variables.start, ' /* Local Variables: */\n'); 32 | expect(variables.end, ' /* End: */'); 33 | expect(variables.trailing, '\n\n'); 34 | expect(variables.entries.length, 2); 35 | expect( 36 | variables.entries[0], 37 | (prefix: ' /* ', content: 'foo: bar ', suffix: '*/\n'), 38 | ); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/org/parser/macro_reference_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('macro reference', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.macroReference()).end(); 9 | test('with args', () { 10 | final result = parser.parse('{{{name(arg1, arg2)}}}'); 11 | final ref = result.value as OrgMacroReference; 12 | expect(ref.content, '{{{name(arg1, arg2)}}}'); 13 | }); 14 | test('simple', () { 15 | final result = parser.parse('{{{foobar}}}'); 16 | final ref = result.value as OrgMacroReference; 17 | expect(ref.content, '{{{foobar}}}'); 18 | }); 19 | test('empty', () { 20 | final result = parser.parse('{{{}}}'); 21 | expect(result, isA(), reason: 'Body missing'); 22 | }); 23 | test('invalid key', () { 24 | final result = parser.parse('{{{0abc}}}'); 25 | expect(result, isA(), reason: 'Invalid key'); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/org/parser/markups_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('markups', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.markups()).end(); 9 | test('with line break', () { 10 | final result = parser.parse('''/foo 11 | bar/'''); 12 | final markup = result.value as OrgMarkup; 13 | final content = markup.content.children.single as OrgPlainText; 14 | expect(content.content, 'foo\nbar'); 15 | expect(markup.leadingDecoration, '/'); 16 | expect(markup.trailingDecoration, '/'); 17 | expect(markup.style, OrgStyle.italic); 18 | }); 19 | test('nested markup', () { 20 | final result = parser.parse("/foo *bar* baz/"); 21 | final markup = result.value as OrgMarkup; 22 | final nested = 23 | markup.find((node) => node.style == OrgStyle.bold); 24 | expect(nested, isNotNull); 25 | expect(nested!.node.content.children.single.toMarkup(), 'bar'); 26 | expect(nested.path.map((n) => n.toString()).toList(), 27 | ['OrgMarkup', 'OrgContent', 'OrgMarkup']); 28 | }); 29 | test('verbatim with code markup', () { 30 | final result = parser.parse("~foo *bar* baz~"); 31 | final markup = result.value as OrgMarkup; 32 | final nested = markup.content.children.single as OrgPlainText; 33 | expect(nested.content, 'foo *bar* baz'); 34 | }); 35 | test('verbatim with verbatim markup', () { 36 | final result = parser.parse("=foo *bar* baz="); 37 | final markup = result.value as OrgMarkup; 38 | final nested = markup.content.children.single as OrgPlainText; 39 | expect(nested.content, 'foo *bar* baz'); 40 | }); 41 | test('double-nested markup', () { 42 | final result = parser.parse("+foo *bar /baz/ buzz* bazinga+"); 43 | final markup = result.value as OrgMarkup; 44 | final nested = 45 | markup.find((node) => node.style == OrgStyle.italic); 46 | expect(nested, isNotNull); 47 | expect(nested!.node.content.children.single.toMarkup(), 'baz'); 48 | expect(nested.path.map((n) => n.toString()).toList(), 49 | ['OrgMarkup', 'OrgContent', 'OrgMarkup', 'OrgContent', 'OrgMarkup']); 50 | }); 51 | test('with too many line breaks', () { 52 | final result = parser.parse('''/foo 53 | 54 | bar/'''); 55 | expect(result, isA()); 56 | }); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /test/org/parser/pgp_block_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('PGP block', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.pgpBlock()).end(); 9 | test('simple', () { 10 | final result = parser.parse('''-----BEGIN PGP MESSAGE----- 11 | 12 | jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O 13 | X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY= 14 | =chda 15 | -----END PGP MESSAGE----- 16 | '''); 17 | final block = result.value as OrgPgpBlock; 18 | expect(block.indent, ''); 19 | expect(block.header, '-----BEGIN PGP MESSAGE-----'); 20 | expect( 21 | block.body, 22 | '\n\n' 23 | 'jA0ECQMIP3AfqImNg7Xy0j8BBJmT8GSO3VIzObhKP4d6rcH3SdhUpI0dnFpg0y+O\n' 24 | 'X0q9CWVysb7ljRYEkpIbFpdKeCtLFBXSJJdCxfKewKY=\n' 25 | '=chda\n', 26 | ); 27 | expect(block.footer, '-----END PGP MESSAGE-----'); 28 | expect(block.trailing, '\n'); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /test/org/parser/planning_entry_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | final definition = OrgContentParserDefinition(); 7 | final parser = definition.buildFrom(definition.planningEntry()).end(); 8 | test('planning line', () { 9 | final result = 10 | parser.parse('CLOCK: [2021-01-23 Sat 09:30]--[2021-01-23 Sat 10:19]'); 11 | final planningLine = result.value as OrgPlanningEntry; 12 | expect(planningLine.keyword.content, 'CLOCK:'); 13 | final value = planningLine.value as OrgDateRangeTimestamp; 14 | expect(value.start.date.year, '2021'); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/org/parser/radio_link_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('radio link', () { 7 | final definition = OrgContentParserDefinition(radioTargets: ['foo', 'bar']); 8 | final parser = definition.buildFrom(definition.radioLink()).end(); 9 | test('simple', () { 10 | final result = parser.parse('foo'); 11 | final target = result.value as OrgRadioLink; 12 | expect(target.content, 'foo'); 13 | }); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/org/parser/radio_target_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('radio target', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.radioTarget()).end(); 9 | test('single character', () { 10 | final result = parser.parse('<<>>'); 11 | final target = result.value as OrgRadioTarget; 12 | expect(target.leading, '<<<'); 13 | expect(target.body, '!'); 14 | expect(target.trailing, '>>>'); 15 | }); 16 | test('multiple words', () { 17 | final result = parser.parse('<<>>'); 18 | final target = result.value as OrgRadioTarget; 19 | expect(target.leading, '<<<'); 20 | expect(target.body, 'foo bar'); 21 | expect(target.trailing, '>>>'); 22 | }); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/org/parser/statistics_cookie_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('markups', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.statsCookie()).end(); 9 | group('percentage', () { 10 | test('simple', () { 11 | final result = 12 | parser.parse('[50%]').value as OrgStatisticsPercentageCookie; 13 | expect(result.leading, '['); 14 | expect(result.percentage, '50'); 15 | expect(result.suffix, '%'); 16 | expect(result.trailing, ']'); 17 | }); 18 | test('empty', () { 19 | final result = 20 | parser.parse('[%]').value as OrgStatisticsPercentageCookie; 21 | expect(result.leading, '['); 22 | expect(result.percentage, ''); 23 | expect(result.suffix, '%'); 24 | expect(result.trailing, ']'); 25 | }); 26 | }); 27 | group('fraction', () { 28 | test('simple', () { 29 | final result = 30 | parser.parse('[1/2]').value as OrgStatisticsFractionCookie; 31 | expect(result.leading, '['); 32 | expect(result.numerator, '1'); 33 | expect(result.separator, '/'); 34 | expect(result.denominator, '2'); 35 | expect(result.trailing, ']'); 36 | }); 37 | test('empty', () { 38 | final result = parser.parse('[/]').value as OrgStatisticsFractionCookie; 39 | expect(result.leading, '['); 40 | expect(result.numerator, ''); 41 | expect(result.separator, '/'); 42 | expect(result.denominator, ''); 43 | expect(result.trailing, ']'); 44 | }); 45 | test('partial', () { 46 | final result = 47 | parser.parse('[/2]').value as OrgStatisticsFractionCookie; 48 | expect(result.leading, '['); 49 | expect(result.numerator, ''); 50 | expect(result.separator, '/'); 51 | expect(result.denominator, '2'); 52 | expect(result.trailing, ']'); 53 | }); 54 | }); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /test/org/parser/subscript_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('subscript', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = 9 | seq2(letter(), definition.buildFrom(definition.subscript())).end(); 10 | test('H2O', () { 11 | final result = parser.parse(r'H_2O'); 12 | final (_, OrgSubscript sup) = result.value; 13 | expect(sup.leading, '_'); 14 | final body = sup.body.children.single as OrgPlainText; 15 | expect(body.content, '2O'); 16 | expect(sup.trailing, ''); 17 | }); 18 | test('with entity', () { 19 | final result = parser.parse(r'a_\alpha'); 20 | final (_, OrgSubscript sup) = result.value; 21 | expect(sup.leading, '_'); 22 | final body = sup.body.children.single as OrgEntity; 23 | expect(body.name, 'alpha'); 24 | expect(sup.trailing, ''); 25 | }); 26 | test('with text and entity', () { 27 | final result = parser.parse(r'a_{1 + \alpha}'); 28 | final (_, OrgSubscript sup) = result.value; 29 | expect(sup.leading, '_{'); 30 | final body1 = sup.body.children[0] as OrgPlainText; 31 | expect(body1.content, '1 + '); 32 | final body2 = sup.body.children[1] as OrgEntity; 33 | expect(body2.name, 'alpha'); 34 | expect(sup.trailing, '}'); 35 | }); 36 | test('nested bracketed expression', () { 37 | final result = parser.parse('a_{a1 {b2}}'); 38 | final (_, OrgSubscript sup) = result.value; 39 | expect(sup.leading, '_{'); 40 | final body = sup.body.children.single as OrgPlainText; 41 | expect(body.content, 'a1 {b2}'); 42 | expect(sup.trailing, '}'); 43 | }); 44 | test('nested sexp', () { 45 | final result = parser.parse('a_(a1 (b2))'); 46 | final (_, OrgSubscript sup) = result.value; 47 | expect(sup.leading, '_'); 48 | final body = sup.body.children.single as OrgPlainText; 49 | expect(body.content, '(a1 (b2))'); 50 | expect(sup.trailing, ''); 51 | }); 52 | test('nested subscript', () { 53 | final result = parser.parse('a_{a1_{b2}}'); 54 | final (_, OrgSubscript sup) = result.value; 55 | final nested = 56 | sup.find((node) => node.toMarkup() == '_{b2}'); 57 | expect(nested, isNotNull); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/org/parser/superscript_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('superscript', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = 9 | seq2(letter(), definition.buildFrom(definition.superscript())).end(); 10 | test('with entity', () { 11 | final result = parser.parse(r'a^\alpha'); 12 | final (_, OrgSuperscript sup) = result.value; 13 | expect(sup.leading, '^'); 14 | final body = sup.body.children.single as OrgEntity; 15 | expect(body.name, 'alpha'); 16 | expect(sup.trailing, ''); 17 | }); 18 | test('with text and entity', () { 19 | final result = parser.parse(r'a^{1 + \alpha}'); 20 | final (_, OrgSuperscript sup) = result.value; 21 | expect(sup.leading, '^{'); 22 | final body1 = sup.body.children[0] as OrgPlainText; 23 | expect(body1.content, '1 + '); 24 | final body2 = sup.body.children[1] as OrgEntity; 25 | expect(body2.name, 'alpha'); 26 | expect(sup.trailing, '}'); 27 | }); 28 | test('nested bracketed expression', () { 29 | final result = parser.parse('a^{a1 {b2}}'); 30 | final (_, OrgSuperscript sup) = result.value; 31 | expect(sup.leading, '^{'); 32 | final body = sup.body.children.single as OrgPlainText; 33 | expect(body.content, 'a1 {b2}'); 34 | expect(sup.trailing, '}'); 35 | }); 36 | test('nested sexp', () { 37 | final result = parser.parse('a^(a1 (b2))'); 38 | final (_, OrgSuperscript sup) = result.value; 39 | expect(sup.leading, '^'); 40 | final body = sup.body.children.single as OrgPlainText; 41 | expect(body.content, '(a1 (b2))'); 42 | expect(sup.trailing, ''); 43 | }); 44 | test('nested superscript', () { 45 | final result = parser.parse('a^{a1^{b2}}'); 46 | final (_, OrgSuperscript sup) = result.value; 47 | final nested = 48 | sup.find((node) => node.toMarkup() == '^{b2}'); 49 | expect(nested, isNotNull); 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /test/org/parser/table_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:petitparser/petitparser.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('table', () { 7 | final definition = OrgContentParserDefinition(); 8 | final parser = definition.buildFrom(definition.table()).end(); 9 | test('simple', () { 10 | final result = parser.parse(''' | foo | *bar* | baz | 11 | |-----+-----+-----| 12 | | 1 | 2 | 3 | 13 | '''); 14 | final table = result.value as OrgTable; 15 | final row0 = table.rows[0] as OrgTableCellRow; 16 | final row0Cell0 = row0.cells[0].content.children[0] as OrgPlainText; 17 | expect(row0Cell0.content, 'foo'); 18 | final row0Cell1 = row0.cells[1].content.children[0] as OrgMarkup; 19 | final row0Cell1Content = 20 | row0Cell1.content.children.single as OrgPlainText; 21 | expect(row0Cell1Content.content, 'bar'); 22 | expect(row0Cell1.leadingDecoration, '*'); 23 | expect(row0Cell1.trailingDecoration, '*'); 24 | expect(row0.cells.length, 3); 25 | expect(table.rows[1], isA()); 26 | final row2 = table.rows[2] as OrgTableCellRow; 27 | expect(row2.cells.length, 3); 28 | }); 29 | test('empty', () { 30 | final result = parser.parse('||'); 31 | final table = result.value as OrgTable; 32 | final row0 = table.rows[0] as OrgTableCellRow; 33 | expect(row0.cells[0].content.children.isEmpty, isTrue); 34 | }); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /test/org/parser/todo_states_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:org_parser/src/todo/todo.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | void main() { 6 | group('todo keywords', () { 7 | final parser = OrgParserDefinition(todoStates: [ 8 | OrgTodoStates(todo: ['FOO'], done: ['BAR']) 9 | ]).build(); 10 | test('custom keywords', () { 11 | final doc = parser.parse(''' 12 | * FOO [#A] foo bar 13 | buzz baz 14 | * BAR [#B] bar foo 15 | baz buzz''').value as OrgDocument; 16 | expect(doc.sections[0].headline.keyword?.value, 'FOO'); 17 | expect(doc.sections[0].headline.keyword?.done, isFalse); 18 | expect(doc.sections[1].headline.keyword?.value, 'BAR'); 19 | expect(doc.sections[1].headline.keyword?.done, isTrue); 20 | }); 21 | test('unrecognized keyword', () { 22 | final doc = parser.parse('''* TODO [#A] foo bar 23 | baz buzz''').value as OrgDocument; 24 | expect(doc.sections[0].headline.keyword?.value, isNull); 25 | expect(doc.sections[0].headline.rawTitle, 'TODO [#A] foo bar'); 26 | }); 27 | test('empty states object', () { 28 | final parser = OrgParserDefinition(todoStates: [OrgTodoStates()]).build(); 29 | final doc = parser.parse('''* TODO [#A] foo bar 30 | baz buzz''').value as OrgDocument; 31 | expect(doc.sections[0].headline.keyword?.value, isNull); 32 | expect(doc.sections[0].headline.rawTitle, 'TODO [#A] foo bar'); 33 | }); 34 | test('empty list of states objects', () { 35 | final parser = OrgParserDefinition(todoStates: []).build(); 36 | final doc = parser.parse('''* TODO [#A] foo bar 37 | baz buzz''').value as OrgDocument; 38 | expect(doc.sections[0].headline.keyword?.value, isNull); 39 | expect(doc.sections[0].headline.rawTitle, 'TODO [#A] foo bar'); 40 | }); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/query/matchers.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/org/org.dart'; 2 | import 'package:org_parser/src/query/query.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | final _sectionParser = (() { 6 | final definition = OrgParserDefinition(); 7 | return definition.buildFrom(definition.section()); 8 | })(); 9 | 10 | Matcher acceptsSection(String sectionMarkup) => 11 | _QueryMatcher(sectionMarkup, true); 12 | Matcher rejectsSection(String sectionMarkup) => 13 | _QueryMatcher(sectionMarkup, false); 14 | 15 | class _QueryMatcher extends Matcher { 16 | _QueryMatcher( 17 | String sectionMarkup, 18 | this.expected, 19 | ) : section = _sectionParser.parse(sectionMarkup).value as OrgSection; 20 | 21 | final OrgSection section; 22 | final bool expected; 23 | 24 | @override 25 | bool matches(dynamic item, Map matchState) { 26 | if (item is! OrgQueryMatcher) return false; 27 | 28 | return item.matches(section) == expected; 29 | } 30 | 31 | @override 32 | Description describe(Description description) => description.add(expected 33 | ? 'The query accepts the section' 34 | : 'The query rejects the section'); 35 | } 36 | -------------------------------------------------------------------------------- /test/url_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:org_parser/src/url.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('local section url parser', () { 6 | expect(true, isOrgLocalSectionUrl('*foo')); 7 | expect(false, isOrgLocalSectionUrl('foo')); 8 | expect('foo bar', parseOrgLocalSectionUrl('*foo bar')); 9 | expect('foo bar', parseOrgLocalSectionUrl('''*foo 10 | bar''')); 11 | }); 12 | test('custom ID url parser', () { 13 | expect(true, isOrgCustomIdUrl('#foo')); 14 | expect(false, isOrgCustomIdUrl('foo')); 15 | expect('foo bar', parseOrgCustomIdUrl('#foo bar')); 16 | }); 17 | test('ID url parser', () { 18 | expect(true, isOrgIdUrl('id:foo')); 19 | expect(false, isOrgIdUrl('foo')); 20 | expect('foo bar', parseOrgIdUrl('id:foo bar')); 21 | }); 22 | } 23 | --------------------------------------------------------------------------------