├── .circleci └── config.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE │ └── pr_template.md ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.org ├── LICENSE ├── Makefile ├── README.org ├── doc └── images │ └── quick_introduction_to_lisp_clojure_and_using_the_repl.jpg ├── project.clj ├── resources └── org.ebnf ├── src └── org_parser │ ├── cli.clj │ ├── core.cljc │ ├── macros.clj │ ├── parser.clj │ ├── parser.cljs │ ├── render.cljc │ └── transform.cljc └── test └── org_parser ├── core_test.cljc ├── fixtures ├── bold_text.org ├── headlines_and_tables.org ├── minimal.org └── schedule_with_repeater.org ├── integration └── section_test.cljc ├── parser_mean_test.cljc ├── parser_test.cljc ├── test_runner.cljs └── transform_test.cljc /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Clojure CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | test-clj: 8 | docker: 9 | - image: circleci/clojure:openjdk-8-lein 10 | working_directory: ~/repo 11 | environment: 12 | LEIN_ROOT: "true" 13 | # Customize the JVM maximum heap limit 14 | JVM_OPTS: -Xmx3200m 15 | 16 | steps: 17 | - checkout 18 | 19 | # Download and cache dependencies 20 | - restore_cache: 21 | keys: 22 | - v1-dependencies-{{ checksum "project.clj" }} 23 | # fallback to using the latest cache if no exact match is found 24 | - v1-dependencies- 25 | 26 | - run: lein deps 27 | - run: lein test 28 | 29 | - save_cache: 30 | paths: 31 | - ~/.m2 32 | key: v1-dependencies-{{ checksum "project.clj" }} 33 | 34 | test-cljs: 35 | docker: 36 | - image: circleci/clojure:openjdk-8-lein-node 37 | working_directory: ~/repo 38 | environment: 39 | LEIN_ROOT: "true" 40 | # Customize the JVM maximum heap limit 41 | JVM_OPTS: -Xmx3200m 42 | 43 | steps: 44 | - checkout 45 | 46 | # Download and cache dependencies 47 | - restore_cache: 48 | keys: 49 | - v1-dependencies-{{ checksum "project.clj" }} 50 | # fallback to using the latest cache if no exact match is found 51 | - v1-dependencies- 52 | 53 | - run: lein deps 54 | - run: lein doo node once 55 | 56 | - save_cache: 57 | paths: 58 | - ~/.m2 59 | key: v1-dependencies-{{ checksum "project.clj" }} 60 | 61 | release: 62 | docker: 63 | - image: circleci/clojure:openjdk-8-lein-bullseye 64 | steps: 65 | - checkout 66 | - restore_cache: 67 | keys: 68 | - v1-dependencies-{{ checksum "project.clj" }} 69 | - v1-dependencies- 70 | - run: 71 | name: Install dependencies 72 | command: sudo apt-get update -yq && sudo apt install -yq git 73 | - run: 74 | name: Configure Git 75 | command: | 76 | git config --global user.email "info@200ok.ch" 77 | git config --global user.name "200ok CI bot" 78 | git remote rm origin > /dev/null 2>&1 79 | # Set up origin with the ability to push to it through 80 | # token auth. 81 | git remote add origin "https://${GH_TOKEN}@github.com/200ok-ch/org-parser.git" > /dev/null 2>&1 82 | # Checkout the relevant branch. `lein release` does not 83 | # work from a commit sha. 84 | git checkout -B "$CIRCLE_BRANCH" 85 | 86 | 87 | - run: 88 | name: Deploy to Clojars 89 | command: | 90 | # Take away the `-SNAPSHOT` postfix from the latest version 91 | lein change version leiningen.release/bump-version release 92 | lein vcs commit "chore: Bump to version %s [skip ci]" 93 | lein vcs tag v --no-sign 94 | # Deploy this version to Clojars 95 | lein update-in :deploy-repositories conj "[\"clojars\" {:url \"https://repo.clojars.org/\" :username :env/clojars_username :password :env/clojars_password :sign-releases false}]" -- deploy clojars 96 | # Bump to the next patch level with `-SNAPSHOT` postfix 97 | lein change version leiningen.release/bump-version 98 | lein vcs commit "chore: Bump to version %s [skip ci]" 99 | git push --set-upstream origin "$CIRCLE_BRANCH" 100 | 101 | workflows: 102 | version: 2 103 | run_tests: 104 | jobs: 105 | - test-clj 106 | - test-cljs 107 | - release: 108 | requires: 109 | - test-clj 110 | - test-cljs 111 | filters: 112 | branches: 113 | only: master 114 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [200ok-ch] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Provide a minimal Org mode code example 17 | 2. Provide parsed data structure or error message 18 | 3. If known, provide the symbol for the parser 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expect to happen instead. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pr_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull Request 3 | about: '' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Short description of this PR. 11 | 12 | [if applicable:] 13 | Fixes #xx 14 | 15 | - list 16 | - of 17 | - concrete 18 | - changes 19 | 20 | -------- 21 | 22 | - [ ] Add tests for your change or feature 23 | - [ ] Update results of usage examples in the [README.org](https://github.com/200ok-ch/org-parser#usage) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | 14 | /out 15 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.17.3 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | ### Changed 6 | - Add a new arity to `make-widget-async` to provide a different widget shape. 7 | 8 | ## [0.1.1] - 2019-08-23 9 | ### Changed 10 | - Documentation on how to make the widgets. 11 | 12 | ### Removed 13 | - `make-widget-sync` - we're all async, all the time. 14 | 15 | ### Fixed 16 | - Fixed widget maker to keep working when daylight savings switches over. 17 | 18 | ## 0.1.0 - 2019-08-23 19 | ### Added 20 | - Files from the new template. 21 | - Widget maker public API - `make-widget-sync`. 22 | 23 | [Unreleased]: https://github.com/your-name/org-parser/compare/0.1.1...HEAD 24 | [0.1.1]: https://github.com/your-name/org-parser/compare/0.1.0...0.1.1 25 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our 7 | project and our community a harassment-free experience for everyone, 8 | regardless of age, body size, disability, ethnicity, gender identity 9 | and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive 16 | environment include: 17 | 18 | - Using welcoming and inclusive language 19 | - Being respectful of differing viewpoints and experiences 20 | - Gracefully accepting constructive criticism 21 | - Focusing on what is best for the community 22 | - Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual 27 | attention or advances 28 | - Trolling, insulting/derogatory comments, and personal or political 29 | attacks 30 | - Public or private harassment 31 | - Publishing others' private information, such as a physical or 32 | electronic address, without explicit permission 33 | - Other conduct which could reasonably be considered inappropriate 34 | in a professional setting 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of 39 | acceptable behavior and are expected to take appropriate and fair 40 | corrective action in response to any instances of unacceptable 41 | behavior. 42 | 43 | Project maintainers have the right and responsibility to remove, edit, 44 | or reject comments, commits, code, wiki edits, issues, and other 45 | contributions that are not aligned to this Code of Conduct, or to ban 46 | temporarily or permanently any contributor for other behaviors that 47 | they deem inappropriate, threatening, offensive, or harmful. 48 | 49 | ## Scope 50 | 51 | This Code of Conduct applies both within project spaces and in public 52 | spaces when an individual is representing the project or its 53 | community. Examples of representing a project or community include 54 | using an official project e-mail address, posting via an official 55 | social media account, or acting as an appointed representative at an 56 | online or offline event. Representation of a project may be further 57 | defined and clarified by project maintainers. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior 62 | may be reported by contacting the project team at info@200ok.ch. All 63 | complaints will be reviewed and investigated and will result in a 64 | response that is deemed necessary and appropriate to the 65 | circumstances. The project team is obligated to maintain 66 | confidentiality with regard to the reporter of an incident. Further 67 | details of specific enforcement policies may be posted separately. 68 | 69 | Project maintainers who do not follow or enforce the Code of Conduct 70 | in good faith may face temporary or permanent repercussions as 71 | determined by other members of the project's leadership. 72 | 73 | Attribution 74 | ----------- 75 | 76 | This Code of Conduct is adapted from the [Contributor 77 | Covenant](https://www.contributor-covenant.org), version 1.4, 78 | available at 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.org: -------------------------------------------------------------------------------- 1 | * Contribute to org-parser 2 | 3 | First off, thank you for considering contributing to org-parser. It's 4 | people like you that make org-parser such a great tool! 5 | 6 | ** Did you find a bug or do you want to implement a new feature? 7 | 8 | What do we consider a bug? A bug is when an existing feature is 9 | broken. 10 | 11 | A feature request is to ask for change in an existing feature or to 12 | ask for a completely new feature. 13 | 14 | Hence, if your request is about a feature that Emacs Org mode has, but 15 | org-parser lacks, this is a feature request, not a bug. 16 | 17 | - Ensure the bug or feature was not already reported by searching on 18 | GitHub under [[https://github.com/200ok-ch/org-parser/issues][Issues]]. 19 | - Ensure that the the bug you want to report is not known behavior by 20 | reading the [[file:README.org][readme]] and the [[https://github.com/200ok-ch/org-parser/wiki][wiki]]. 21 | - If you're unable to find an open issue addressing the problem, [[https://github.com/200ok-ch/org-parser/issues/new][open 22 | a new one]]. 23 | - If you want to report a bug or request a feature, please use the 24 | "Bug report" or "Feature request" ticket and fill out the asked 25 | for metadata. 26 | - If your request is neither about a bug nor feature, please do open 27 | a blank issue. 28 | 29 | ** How we work with issues 30 | 31 | One of our quality goals is to keep the amount of issues manageable. 32 | This means that a single person has to be able to groom the backlog 33 | (review the open issues) in a reasonable amount of time. 34 | 35 | These are the steps we take to ensure a high quality in the backlog: 36 | 37 | 1. We only keep issues open on a longer term basis that fulfill one of 38 | the following criteria 39 | - Someone has committed to work on it or plausibly plans to do so 40 | in the future 41 | - It is a bug or regression 42 | 2. If someone files a bug or feature request which is either not clear 43 | to the maintainers or is likely not a bug, but known and documented 44 | behavior, we ask a question or link to the documentation. We also 45 | add a 'question' label to the issue. If the issue creator does not 46 | respond to the question within 5 days, we close the issue. If she 47 | does choose to respond at some point, she can re-open the issue. 48 | 49 | ** Development collaboration 50 | 51 | We have good quality assurance and an established workflow. This is it: 52 | 53 | 1. Open a new GitHub pull request 54 | 2. Ensure the PR title and description clearly describes the problem 55 | and solution. Include the relevant issue number if applicable. 56 | 3. New Branch in Git 57 | 4. Naming: =(feature|fix|chore)/issue-id/short-description= 58 | 5. Develop / Test locally until tests pass 59 | - If you're using Emacs, you can autoformat your source files using 60 | [[https://docs.cider.mx][Cider]] =cider-format-buffer=: 61 | https://github.com/munen/emacs.d/#auto-formatting 62 | 6. Create Pull Request on Github to =master= 63 | - Check that the tests also pass on CI 64 | 7. Core Team: Merge to =master=, deploy and accept the Issue on 65 | Github 66 | 67 | *** Definition of Done 68 | 69 | An issue is done when: 70 | 71 | - Functionality has been implemented 72 | - Functionality has been verified 73 | - Surrounding main functionality has been regression tested 74 | - Code has been reviewed 75 | - Proper code style 76 | - Code has tests (acceptance tests for user visible changes, 77 | otherwise at least unit tests) 78 | - Follows clean code guidelines and architecture best practices 79 | - Code has been documented 80 | - Code has been merged 81 | - Tested on supported browsers 82 | 83 | *Clean code guidelines* 84 | 85 | - Create small and highly cohesive modules 86 | - Avoid long modules 87 | - Extract modules to separate responsibilities 88 | - Create small methods or functions 89 | - Avoid long methods or functions 90 | - Extract methods or functions to separate responsibilities 91 | - Do one thing 92 | 93 | Thanks! 🙏 🙇 94 | 95 | org-parser Team 96 | 97 | [[https://200ok.ch][200ok llc]] and all 98 | [[https://github.com/200ok-ch/org-parser/graphs/contributors][contributors]] 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | .PHONY: build test testcode testjs testjar clean 3 | 4 | build: buildjar.sh 5 | bash buildjar.sh 6 | # Order is important because one command deletes build artifacts of the other. 7 | # Also, not sure if this would stop correctly on error: 8 | # echo $^ | xargs -n 1 bash 9 | 10 | test: testcode-clj testcode-cljs testjar 11 | 12 | testcode-clj: 13 | lein test 14 | 15 | testcode-cljs: 16 | lein doo node once 17 | 18 | # Test by executing parser on test file and expect at least one line of output (grep .) 19 | testjar: build testjar.sh 20 | bash testjar.sh | grep . 21 | 22 | # Filenames of :tangle files must be hardcoded :( 23 | buildjs.sh buildjar.sh testjar.sh: README.org 24 | emacs --batch -l org $< -f org-babel-tangle 25 | 26 | clean: 27 | $(RM) build*.sh 28 | $(RM) test*.sh 29 | $(RM) target/ 30 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * General 2 | 3 | #+html:

Tests:

4 | 5 | #+html: Clojars Project 6 | 7 | Community chat: #organice on IRC [[https://libera.chat/][Libera.Chat]], or [[https://matrix.to/#/!DfVpGxoYxpbfAhuimY:matrix.org?via=matrix.org&via=ungleich.ch][#organice:matrix.org]] on Matrix 8 | 9 | ** What does this project do? 10 | 11 | =org-parser= is a parser for the [[https://orgmode.org/][Org mode]] markup language for Emacs. 12 | 13 | It can be used from JavaScript, Java, Clojure and ClojureScript! 14 | 15 | ** Why is this project useful / Rationale 16 | 17 | Org mode in Emacs is implemented in [[http://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/org-element.el][org-element.el]] ([[https://orgmode.org/worg/dev/org-element-api.html][API 18 | documentation]]). The [[https://orgmode.org/worg/dev/org-syntax.html][spec for the Org syntax is written in prose]]. 19 | 20 | This is already great work, yet it has some drawbacks: 21 | 22 | 1. The spec is not machine readable. Hence, there can be drift between 23 | documentation and implementation. In fact, during the development 24 | of [[https://github.com/200ok-ch/organice/][organice]], our web-based Org implementation with great mobile 25 | phone support, and =org-parser= we have encountered drift. 26 | 2. org-element.el is naturally written in Emacs lisp and makes strong 27 | use of Emacs as a text-processor. Hence, its code can only be used 28 | within Emacs. 29 | 30 | While writing the official spec already is an amazing effort in the 31 | standardization of the Org format, the power of Org is so enticing 32 | that many want to use it outside of Emacs, as well. Since 33 | org-element.el only runs in Emacs, this caused a myriad of 34 | implementations for other platforms (JavaScript, Rust, Go, Java, etc) 35 | to have been created. Most implementations are only partial, and more 36 | importantly each of them creates another island. Since they are just 37 | as programming language dependent as org-element.el, it is impossible 38 | to share logic between them. 39 | 40 | =org-parser= aims at alleviating both these issues. It documents the 41 | syntax in a standard and machine readable notation (EBNF). And the 42 | reference implementation is done in a way that it runs on the 43 | established virtual machines of Java and JavaScript. Hence, 44 | =org-parser= can be used from all programming languages running on 45 | those virtual machines. =org-parser= provides a higher-level data 46 | structure that is easy to consume for an application working with Org 47 | mode data. Even if your application is not running on the Java or 48 | JavaScript virtual machines, you can embed =org-parser= as a 49 | command-line application. Lastly, =org-parser= brings a strong test 50 | suite to document the reference implementation in yet another 51 | unambiguous way. 52 | 53 | It is our aim that =org-parser= can be the foundation on which many 54 | Org mode applications in many different languages can be built. The 55 | applications using =org-parser= can then focus on implementing user 56 | facing features and don’t have to worry about the implementation of 57 | the Org syntax itself. 58 | 59 | ** Architecture 60 | 61 | The code base of =org-parser= is split into four namespaces: 62 | 63 | - org-parser.core (top level api, i.e. =read-str=, =write-str=) 64 | - org-parser.parse (aka. deserializer, reader) 65 | - org-parser.parse.transform (transforms the result of the parser into 66 | a more desirable structure) 67 | - org-parser.render (aka. serializer, writer) 68 | 69 | Thus =org-parser= has become a misnomer in the sense, that it now 70 | strives to be =clojure/data.org= (after the pattern of existing Clojure 71 | libraries like =data.json=, =data.xml=, =data.csv=, etc) providing 72 | reader as well as writer capabilities for the serialization format 73 | =org=. 74 | 75 | * Project State 76 | 77 | This project is work-in-progress. It is not ready for production yet 78 | because the structure of the AST (parse tree) can still change. 79 | 80 | The biggest milestones are: 81 | 82 | - [X] Finish [[http://xahlee.info/clojure/clojure_instaparse_grammar_syntax.html][EBNF parser]] to support most Org mode syntax 83 | - [X] Headlines 84 | - [X] Org mode =#+*= stuff 85 | - [X] Timestamps 86 | - [X] Links 87 | - [X] Text links 88 | - [X] Footnotes 89 | - [X] Styled text 90 | - [X] Drawers and =#+BEGIN_xxx= blocks 91 | - [ ] Nested markup (see #12) 92 | - [X] Setup basic transformation from the parse tree to a higher-level structure. 93 | - [-] Transformations to higher-level structure: catch up with 94 | features that are already supported by the EBNF parser. 95 | - [-] Render parsed org file with =write-str= 96 | 97 | *It can already be useful* for you: 98 | E.g. if your script needs to parse parts of Org mode features, our EBNF 99 | parser probably already supports that. Do not underestimate 100 | e.g. timestamps. Use our well-tested parser to disassemble it in its 101 | parts, instead of trying to write a poor and ugly regex that is only 102 | capable of a subset of Org mode's timestamps ;) 103 | 104 | Don't hesitate to contribute! 105 | 106 | * Development 107 | 108 | =org-parser= uses [[https://github.com/Engelberg/instaparse/][instaparse]] which aims to be the simplest way to 109 | build parsers in Clojure. Apart from living up to this claim (and 110 | beyond the scope of just the one programming language), using 111 | instaparse is great for another reason: Instaparse works both on CLJ 112 | and CLJS. Therefore =org-parser= can be used from both ecosystems 113 | which, of course, include JavaScript and Java. Hence, it is possible 114 | to [[#usage][use it]] in various situations. 115 | 116 | ** Prerequisites 117 | 118 | Please install [[https://clojure.org/guides/getting_started][Clojure]] and [[https://leiningen.org/][Leiningen]]. 119 | 120 | There's no additional installation required. Leiningen will pull 121 | dependencies if required. 122 | 123 | ** Testing 124 | 125 | Running the tests: 126 | 127 | #+BEGIN_SRC shell 128 | # Clojure 129 | lein test 130 | # CLJS (starts a watcher) 131 | lein doo node 132 | #+END_SRC 133 | 134 | If you're not familiar with Lisp or Clojure, here's a short video on 135 | how the tooling for Lisp (and hence Clojure) is great and enables fast 136 | developer feedback and high quality applications. Initially, the video 137 | was created to answer a [[https://github.com/200ok-ch/org-parser/issues/4][specific issue]] on this repository. However, the question is a valid 138 | general question that is asked quite often by people who haven't used 139 | a Lisp before. 140 | 141 | [[https://raw.githubusercontent.com/200ok-ch/org-parser/master/doc/images/quick_introduction_to_lisp_clojure_and_using_the_repl.jpg]] 142 | 143 | You can watch it here: https://youtu.be/o2MLHFGUkoQ 144 | 145 | * Release and Dependency Information 146 | 147 | Note: The version number should be replaced with the current version of org-parser. 148 | See the clojars badge at the [[https://github.com/200ok-ch/org-parser#general][top of this README]]. 149 | 150 | ** [[https://clojure.org/reference/deps_and_cli][CLI/deps.edn]] dependency information: 151 | 152 | #+BEGIN_SRC 153 | org-parser/org-parser {:mvn/version "0.1.4"} 154 | #+END_SRC 155 | 156 | ** [[https://github.com/technomancy/leiningen][Leiningen]] dependency information: 157 | 158 | #+BEGIN_SRC 159 | [org-parser "0.1.4"] 160 | #+END_SRC 161 | 162 | * Usage 163 | :PROPERTIES: 164 | :CUSTOM_ID: usage 165 | :END: 166 | 167 | At the moment, you can run =org-parser= from Clojure, ClojureScript, 168 | or Java. Other targets which are hosted on the JVM or on JavaScript 169 | are possible. 170 | 171 | ** Clojure Library 172 | 173 | #+BEGIN_SRC clojure :exports both 174 | (ns hello-world.core 175 | (:require [org-parser.parser :refer [parse]] 176 | [org-parser.core :refer [read-str write-str]])) 177 | 178 | (prn (parse "* Headline")) 179 | (prn (read-str "* Headline")) 180 | (println (write-str (read-str "* Headline"))) 181 | #+END_SRC 182 | 183 | #+RESULTS: 184 | | [:S [:headline [:stars "*"] [:text [:text-normal "Headline"]]]] | 185 | | {:headlines [{:headline {:level 1, :title [[:text-normal "Headline"]], :planning [], :tags []}}]} | 186 | | "* Headline\n" | 187 | 188 | ** Clojure 189 | 190 | Run =lein run file.org=, for example: 191 | 192 | #+begin_src sh :results verbatim :exports both 193 | lein run test/org_parser/fixtures/schedule_with_repeater.org 194 | #+end_src 195 | 196 | #+RESULTS: 197 | : {:headlines [{:headline {:level 1, :title [[:text-sty-bold "Header"] [:text-normal " with repeater"]], :planning [[:planning-info [:planning-keyword [:planning-kw-scheduled]] [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2019-11-27"] [:ts-day "Wed"]] [:ts-modifiers [:ts-repeater [:ts-repeater-type "+"] [:ts-mod-value "1"] [:ts-mod-unit "d"]]]]]]], :tags []}}]} 198 | 199 | ** Java 200 | 201 | First, compile =org-parser= with: 202 | 203 | #+begin_src sh :exports code :results silent :tangle buildjar.sh 204 | lein uberjar 205 | #+end_src 206 | 207 | Then run =java -jar target/uberjar/org-parser-*-SNAPSHOT-standalone.jar file.org=, for example: 208 | 209 | #+begin_src sh :results verbatim :exports both :tangle testjar.sh 210 | java -jar target/uberjar/org-parser-*-SNAPSHOT-standalone.jar test/org_parser/fixtures/schedule_with_repeater.org 211 | #+end_src 212 | 213 | #+RESULTS: 214 | : {:headlines [{:headline {:level 1, :title [[:text-sty-bold "Header"] [:text-normal " with repeater"]], :planning [[:planning-info [:planning-keyword [:planning-kw-scheduled]] [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2019-11-27"] [:ts-day "Wed"]] [:ts-modifiers [:ts-repeater [:ts-repeater-type "+"] [:ts-mod-value "1"] [:ts-mod-unit "d"]]]]]]], :tags []}}]} 215 | 216 | Note: The =*= character must be replaced with the current version number of org-parser. 217 | See the clojars badge at the [[https://github.com/200ok-ch/org-parser#general][top of this README]]. 218 | 219 | * License 220 | -------------------------------------------------------------------------------- /doc/images/quick_introduction_to_lisp_clojure_and_using_the_repl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/200ok-ch/org-parser/642ec8be2606ed10b214369a9dca149103a89462/doc/images/quick_introduction_to_lisp_clojure_and_using_the_repl.jpg -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org-parser "0.1.28-SNAPSHOT" 2 | :description "A parser for the Org mode markup language for Emacs" 3 | :url "https://github.com/200ok-ch/org-parser" 4 | :license {:name "GNU Affero General Public License v3.0" 5 | :url "https://www.gnu.org/licenses/agpl-3.0.en.html"} 6 | :dependencies [[org.clojure/clojure "1.10.0"] 7 | [org.clojure/clojurescript "1.10.866"] 8 | [cljs-node-io "1.1.2"] 9 | [org.clojure/data.json "1.0.0"] 10 | [instaparse "1.4.10"]] 11 | :main ^:skip-aot org-parser.cli 12 | :target-path "target/%s" 13 | :repl-options {:init-ns org-parser.core} 14 | :plugins [[lein-cljsbuild "1.1.8"] 15 | [lein-doo "0.1.10"]] 16 | :profiles {:uberjar {:aot :all}} 17 | :doo {:build "test-cljs-with-node"} 18 | :cljsbuild 19 | {:builds [{:id "main" 20 | :source-paths ["src"] 21 | :compiler {:optimizations :none 22 | :target :nodejs 23 | :output-to "target/org-parser.js" 24 | :main org-parser.core 25 | :pretty-print true}} 26 | {:id "test-cljs-with-node" 27 | :source-paths ["src" "test"] 28 | :compiler {:main org-parser.test-runner 29 | :target :nodejs 30 | :output-to 31 | "out/test_cljs_with_node.js" 32 | :optimizations :none}}]}) 33 | -------------------------------------------------------------------------------- /resources/org.ebnf: -------------------------------------------------------------------------------- 1 | 2 | (* Main sources: 3 | - Org Mode Spec: https://orgmode.org/worg/dev/org-syntax.html 4 | - Org Mode Manual: https://www.gnu.org/software/emacs/manual/html_mono/org.html 5 | - Org Mode Compact Guide: https://orgmode.org/guide/ 6 | *) 7 | 8 | S = line* 9 | 10 | = (empty-line / headline / clock / diary-sexp / 11 | comment-line / todo-line / 12 | block-begin-line / block-end-line / 13 | dynamic-block-begin-line / dynamic-block-end-line / 14 | drawer-end-line / drawer-begin-line / 15 | list-item-line / footnote-line / fixed-width-line / 16 | horizontal-rule / table / 17 | affiliated-keyword-line / macro-definition / other-keyword-line / 18 | content-line) eol 19 | 20 | (* TODO delete empty-line token? because it discards whitespace which may not be desired. *) 21 | empty-line = "" | #"\s+" 22 | content-line = text 23 | 24 | (* "Comments consist of one or more consecutive comment lines." 25 | https://orgmode.org/worg/dev/org-syntax.html#Comments *) 26 | comment-line = comment-line-head comment-line-rest 27 | comment-line-head = #"\s*#(?= |$)" 28 | comment-line-rest = #".*" 29 | 30 | = <#'[\r\n]|$'> 31 | = <#"[\t ]+"> 32 | = #"[^\r\n\s]+" 33 | (* indentation matters, e.g. for list-item-line *) 34 | indent = #"[\t ]*" 35 | 36 | = #"\S+" 37 | = #"[^\r\n]+" 38 | 39 | (* It's impractical to parse tags here because the title has to be 40 | parsed as text and text does not know where tags start. 41 | Even if I make text-normal stopping at ':', it still would parse 42 | more text after that. 43 | No problem, tags are extracted in the transformation step. *) 44 | headline = stars [s keyword] [s priority] [s comment-token] s title [ eol planning ] 45 | stars = #'\*+' 46 | keyword = !comment-token #"[A-Z]+" 47 | priority = <"[#"> #"[A-Z]" <"]"> 48 | comment-token = <"COMMENT"> 49 | = text 50 | tags = <':'> ( tag <':'> )+ 51 | <tag> = #'[a-zA-Z0-9_@#%]+' 52 | 53 | (* Fixed Width Areas 54 | https://orgmode.org/worg/dev/org-syntax.html#Fixed_Width_Areas 55 | https://orgmode.org/manual/Literal-Examples.html *) 56 | fixed-width-area = fixed-width-line <eol> { fixed-width-line <eol> } 57 | fixed-width-line = <fw-head> fw-rest 58 | fw-head = #"\s*:( |$)" 59 | <fw-rest> = #".*" 60 | 61 | (* Affiliated Keywords 62 | https://orgmode.org/worg/dev/org-syntax.html#Affiliated_Keywords 63 | 64 | TODO: is macro-definition also an affiliated keyword? if yes, how to combine? 65 | TODO: allow any other keywords beside key and attr 66 | TODO: tests for macros 67 | *) 68 | affiliated-keyword-line = [s] <"#+"> (affil-kw-key | affil-kw-attr) <":"> s kw-value 69 | affil-kw-key = "HEADER" | "NAME" | "PLOT" | (("RESULTS" | "CAPTION") [ affil-kw-optional ]) | "AUTHOR" | "DATE" | "TITLE" 70 | affil-kw-attr = <"ATTR_"> affil-kw-backend 71 | affil-kw-optional = <"["> affil-kw-optional-value <"]"> 72 | <affil-kw-optional-value> = #"[^\]\r\n]+" 73 | affil-kw-value = #"[^\]\r\n]+" 74 | affil-kw-backend = #"[a-zA-Z0-9-_]+" 75 | 76 | (* These are related to Affiliated Keywords but not limited to the 77 | well-known keywords like TITLE, AUTHOR, etc. *) 78 | other-keyword-line = [s] <"#+"> kw-name <":"> s kw-value 79 | kw-name = #"[a-zA-Z0-9-_]+" 80 | kw-value = #"[^\r\n]*" 81 | 82 | macro-definition = [s] <#"#\+(MACRO|macro):"> s macro-name s macro-value 83 | macro-value = anything-but-newline 84 | 85 | (* https://orgmode.org/worg/dev/org-syntax.html#Horizontal_Rules *) 86 | horizontal-rule = #"\s*-----+" 87 | 88 | (* TODO add equivalent TODO keyword specifiers 89 | and allow more than [A-Z] in todo keyword names, esp. the shorthand definitions(d) 90 | https://orgmode.org/manual/Per_002dfile-keywords.html *) 91 | todo-line = <"#+TODO: "> states 92 | <states> = todo-state {s todo-state} <s "|" s> done-state {s done-state} 93 | todo-state = #"[A-Z]+" 94 | done-state = #"[A-Z]+" 95 | 96 | (* Blocks and Greater Blocks 97 | https://orgmode.org/worg/dev/org-syntax.html#Blocks 98 | https://orgmode.org/worg/dev/org-syntax.html#Greater_Blocks 99 | 100 | Both are syntax like #+BEGIN_NAME xxx … #+END_NAME 101 | 102 | Greater blocks can contain "any other element or greater element" 103 | except elements of their own type (i.e. no blocks!) and some other. 104 | 105 | TODO currently blocks have many problems: 106 | - content is parsed greedy (doesn't stop at first #+end_name) 107 | - name is not matched in #+end_name 108 | *) 109 | block = noparse-block / greater-block 110 | block-begin-marker = #'#\+(BEGIN|begin)_' 111 | block-end-marker = #'#\+(END|end)_' 112 | 113 | (* Blocks where content is not parsed *) 114 | noparse-block = noparse-block-begin-line <eol> noparse-block-content block-end-line 115 | noparse-block-begin-line = [s] <block-begin-marker> block-name-noparse [s block-parameters] [s] 116 | (* TODO further divide the noparse-block: src and export blocks require a special syntax for block-parameters, see below *) 117 | noparse-block-content = #'((.|[\r\n])*?[\r\n](?=[\t ]*#\+(END|end)_))|' 118 | 119 | (* Greater "normal" blocks where content is parsed *) 120 | greater-block = block-begin-line <eol> line* block-end-line 121 | (* TODO use negative look-ahead to stop before first block-end-line *) 122 | block-begin-line = [s] <block-begin-marker> block-name [s block-parameters] [s] 123 | block-name = #"\S+" 124 | block-parameters = anything-but-newline 125 | block-end-line = [s] <block-end-marker> block-name [s] 126 | 127 | (* Data/parameters of blocks (coming after #+BEGIN_NAME) *) 128 | block-export-data = #"\w+" 129 | block-src-data = block-src-lang (* [block-src-switches] [block-src-args] *) 130 | block-src-lang = #"\S+" 131 | 132 | (* Block types. Content of blocks with block-name-noparse is NOT parsed. *) 133 | block-name-noparse = #"COMMENT|comment|EXAMPLE|example|EXPORT|export|SRC|src" 134 | block-name-verse = #"VERSE|verse" 135 | block-name-greater = #"CENTER|center|QUOTE|quote" 136 | block-name-special = #"\S+" 137 | 138 | (* Drawers 139 | https://orgmode.org/manual/Drawers.html 140 | https://orgmode.org/worg/dev/org-syntax.html#Drawers 141 | https://orgmode.org/worg/dev/org-syntax.html#Property_Drawers 142 | *) 143 | drawer = drawer-begin-line <eol> line* <drawer-end-line> 144 | drawer-begin-line = <':'> drawer-name <':'> [s] 145 | drawer-name = #"[-\w]+" 146 | drawer-end-line = <':END:'> [s] 147 | 148 | property-drawer = <property-drawer-begin-line> <eol> node-property-line* <drawer-end-line> 149 | property-drawer-begin-line = <':PROPERTIES:'> [s] 150 | 151 | (* Dynamic Blocks 152 | https://orgmode.org/manual/Dynamic-Blocks.html 153 | https://orgmode.org/worg/dev/org-syntax.html#Dynamic_Blocks 154 | *) 155 | dynamic-block = dynamic-block-begin-line <eol> line* <dynamic-block-end-line> 156 | dynamic-block-begin-line = <#"#\+(BEGIN|begin):"> s dynamic-block-name [s dynamic-block-parameters] [s] 157 | dynamic-block-name = anything-but-whitespace 158 | dynamic-block-parameters = anything-but-newline 159 | dynamic-block-end-line = <#"#\+(END|end):"> [s] 160 | 161 | (* Footnotes 162 | https://www.gnu.org/software/emacs/manual/html_node/org/Footnotes.html 163 | *) 164 | footnote-link = <'[fn:'> [ fn-label ] <':'> fn-text-inline <']'> / <'[fn:'> fn-label <']'> 165 | footnote-line = <'[fn:'> fn-label <'] '> fn-text 166 | fn-label = #"[a-zA-Z0-9-_]+" 167 | <fn-text> = text 168 | <fn-text-inline> = #"[^\[\]]*" 169 | 170 | (* Lists 171 | https://orgmode.org/worg/dev/org-syntax.html#Plain_Lists_and_Items 172 | 173 | CHECK-BOX is either a single whitespace character, a "X" character 174 | or a hyphen, enclosed within square brackets. 175 | 176 | Lists cannot be parsed as semantic elements because: 177 | - the definition of where an item ends is complicated (among other 178 | things it depends on indentation) 179 | - lists can be nested and that also depends on indentation 180 | *) 181 | list-item-line = indent ( list-item-bullet | list-item-counter list-item-counter-suffix ) <" "> ( list-item-checkbox <" "> list-item-rest / list-item-rest ) 182 | list-item-bullet = #"[*\-+]" 183 | list-item-counter = #"(\d+|[a-zA-Z])" 184 | list-item-counter-suffix = #"[.)]" 185 | list-item-checkbox = <"["> list-item-checkbox-state <"]"> 186 | list-item-checkbox-state = #"[ \-X]" 187 | list-item-tag = #".*?(?= :: )" (* shortest match followed by " :: " *) 188 | <list-item-rest> = list-item-tag <" :: "> list-item-contents / list-item-contents 189 | <list-item-contents> = text 190 | 191 | (* TODO allow empty properties with or without trailing space *) 192 | node-property-line = ! <':END:'> <':'> node-property-name [node-property-plus] <':'> ( <' '> node-property-value | [<' '>] ) <eol> 193 | node-property-name = #"[^\s:+]+" 194 | node-property-plus = <"+"> 195 | node-property-value = text 196 | 197 | 198 | (* timestamps 199 | https://orgmode.org/worg/dev/org-element-api.html 200 | https://orgmode.org/worg/dev/org-syntax.html#Timestamp 201 | https://orgmode.org/manual/Timestamps.html 202 | 203 | The symbol names are carefully chosen as a trade-off between 204 | shortness and consistency. They have a prefix, e.g. ts-mod-* for 205 | "timestamp modifier", to not get name colissions with other org 206 | mode syntax elements. 207 | 208 | We do not try to reproduce the same structure and symbol names as 209 | in the org mode spec because 210 | 211 | - it's not possible, e.g. :raw-value cannot be preserved with 212 | instaparse (to my understanding) 213 | - split up a date or time in its parts does not make much sense in 214 | a high-level programming language 215 | - e.g. values of :type, :repeater-type, :warning-type cannot be 216 | constructed with instaparse (to my understanding) 217 | 218 | *) 219 | 220 | timestamp = timestamp-diary / timestamp-active / timestamp-inactive 221 | 222 | (* "SEXP can contain any character excepted > and \n." *) 223 | timestamp-diary = <'<%%'> ts-diary-sexp <'>'> 224 | <ts-diary-sexp> = #"[^>\n]*" 225 | 226 | (* TODO How does that work out: :ts-inner appearing two times in :timestamp-active. 227 | And if it works in clojure, does it also work in JS hashes (parse result)? *) 228 | 229 | timestamp-active = <'<'> (ts-inner / ts-inner-span) <'>'> / <'<'> ts-inner <'>--<'> ts-inner <'>'> 230 | 231 | timestamp-inactive = <'['> (ts-inner / ts-inner-span) <']'> / <'['> ts-inner <']--['> ts-inner <']'> 232 | 233 | (* Used for CLOCK entries: *) 234 | timestamp-inactive-range = <'['> ts-inner-span <']'> / <'['> ts-inner-w-time <']--['> ts-inner-w-time <']'> 235 | timestamp-inactive-no-range = <'['> ts-inner-w-time <']'> 236 | 237 | (* a single point in time *) 238 | ts-inner = ts-inner-w-time ts-modifiers / ts-inner-wo-time ts-modifiers 239 | 240 | (* a time span *) 241 | ts-inner-span = ts-inner-w-time <'-'> ts-time ts-modifiers 242 | 243 | ts-inner-w-time = ts-date [<' '+> ts-day] <' '+> ts-time 244 | ts-inner-wo-time = ts-date [<' '+> ts-day] 245 | 246 | ts-date = #"\d{4}-\d{2}-\d{2}" 247 | 248 | (* It is possible to implement stricter rules, e.g. regex [012]?\d for hours. 249 | However, it would add complexity and date/time must be validated at 250 | a higher level anyway. Additionally, orgmode C-c C-c seems to add 251 | date and time; if time is "too big" and points to the next day, the 252 | timestamp date is updated accordingly. *) 253 | ts-time = #"\d{1,2}:\d{2}(:\d{2})?([AaPp][Mm])?" 254 | 255 | (* TODO Use this regex for unicode letters: #"\p{L}+" (only works in JVM) *) 256 | ts-day = #"[^\d\s>\]]+" 257 | 258 | (* Reapeaters and warnings are described here: 259 | https://orgmode.org/manual/Repeated-tasks.html *) 260 | ts-modifiers = Epsilon | (<' '+> ts-repeater [<' '+> ts-warning]) | (<' '+> ts-warning [<' '+> ts-repeater]) 261 | 262 | ts-repeater = ts-repeater-type ts-mod-value ts-mod-unit [<'/'> ts-mod-at-least] 263 | ts-warning = ts-warning-type ts-mod-value ts-mod-unit 264 | 265 | (* See https://orgmode.org/manual/Tracking-your-habits.html *) 266 | ts-mod-at-least = ts-mod-value ts-mod-unit 267 | 268 | ts-repeater-type = ('+'|'++'|'.+') 269 | ts-warning-type = ('-'|'--') 270 | 271 | ts-mod-value = #'\d+' 272 | ts-mod-unit = #'[hdwmy]' 273 | 274 | 275 | 276 | 277 | 278 | (* text is any orgmode text that can contain markup, links, footnotes, timestamps, ... 279 | 280 | It can be a full line or part of a line (e.g. in title, lists, property values, tables, ...) 281 | 282 | *) 283 | 284 | (* TODO handle latex code? *) 285 | text = { timestamp / link-format / footnote-link / text-link / 286 | text-target / text-radio-target / text-entity / text-macro / 287 | text-styled / text-sub / text-sup / text-linebreak / text-normal } 288 | 289 | (* Emphasis and Monospace (font style markup) 290 | https://orgmode.org/manual/Emphasis-and-Monospace.html 291 | "Text in the code and verbatim string is not processed for Org specific syntax;" 292 | Implies that the rest can be nested. 293 | *) 294 | <text-styled> = text-sty-bold / text-sty-italic / text-sty-underlined / text-sty-strikethrough / text-sty-verbatim / text-sty-code 295 | 296 | (* Do not try to parse styled text recursively. Only parse simplest form of styled text. *) 297 | (* TODO simplest possible solution; ignores ways of escaping and does not allow the delim to appear inside *) 298 | text-sty-bold = <'*'> text-inside-sty-normal <'*'> 299 | text-sty-italic = <'/'> text-inside-sty-normal <'/'> 300 | text-sty-underlined = <'_'> text-inside-sty-normal <'_'> 301 | text-sty-strikethrough = <'+'> text-inside-sty-normal <'+'> 302 | (* https://orgmode.org/worg/dev/org-syntax.html#Emphasis_Markers is wrong at this point: 303 | The BORDER character in =BxB= must not be whitespace but can be [,'"]. *) 304 | text-sty-verbatim = <'='> #"([^\s]|[^\s].*?[^\s])(?==($|[- \t.,:!?;'\")}\[]))" <'='> 305 | text-sty-code = <'~'> #"([^\s]|[^\s].*?[^\s])(?=~($|[- \t.,:!?;'\")}\[]))" <'~'> 306 | 307 | (* taken from org-emph-re/org-verbatim-re *) 308 | before-sty = #"[- ('\"{]|" 309 | 310 | (* first and last character must not be whitespace. that's why it's not just text *) 311 | text-inside-sty = ( link-format / footnote-link / text-link / text-styled / text-inside-sty-normal )* 312 | (* TODO space works? includes newline? *) 313 | <text-inside-sty-normal> = #"([^ ]|[^ ].*?[^ ])(?=[*/_+]([- .,:!?;'\")}\[]|$))" 314 | 315 | (* There are 4 types of links: radio link (not subject to this 316 | parser), angle link, plain link, and regular link 317 | 318 | https://orgmode.org/worg/dev/org-syntax.html#Links 319 | 320 | Plain links are defined here but probably never matched because 321 | they have no characteristic start delimiter. 322 | 323 | Protocol must match org-link-types-re but are here defined more 324 | open. We can't rely on variable org settings. 325 | *) 326 | text-link = text-link-angle / text-link-plain 327 | text-link-angle = <'<'> link-url-scheme <':'> text-link-angle-path <'>'> 328 | text-link-angle-path = #"[^\]<>\n]+" 329 | text-link-plain = link-url-scheme <':'> text-link-plain-path 330 | text-link-plain-path = #"[^\s()<>]+(\w|[^\s[:punct:]]/)" 331 | 332 | (* TODO how to prevent greedyness? e.g. not parse text-link-plain (-> look-ahead?) *) 333 | (* Simple work-around: parse characters as long there is no 334 | characteristic delimiter – but always parse at least one character. 335 | This works so far because when parsing, text-normal is tried last. 336 | It can result in multiple subsequent text-normal which will be 337 | concated in a later transform step. 338 | 339 | Stop parsing at EOL. 340 | *) 341 | text-normal = #".[^*/_=~+\[<{^\\\n\r]*" 342 | 343 | (* Superscript and subscript 344 | https://orgmode.org/worg/dev/org-syntax.html#Subscript_and_Superscript 345 | https://orgmode.org/manual/Subscripts-and-Superscripts.html *) 346 | text-sub = <'_'> ( text-subsup-curly | text-subsup-word ) 347 | text-sup = <'^'> ( text-subsup-curly | text-subsup-word ) 348 | (* TODO word should match any unicode letter or digit but not "_"; 349 | in Java regex this is possible with sth. like [\p{L}&&[^_]] 350 | https://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html 351 | *) 352 | text-subsup-word = #"\*|[+-]?[a-zA-Z0-9,.\\]*[a-zA-Z0-9]" 353 | text-subsup-curly = <'{'> #"[^{}]*" <'}'> 354 | 355 | (* https://orgmode.org/worg/dev/org-syntax.html#Targets_and_Radio_Targets *) 356 | text-target = <'<<'> text-target-name <'>>'> 357 | text-target-name = #"[^<>\n\s][^<>\n]*[^<>\n\s]" | #"[^<>\n\s]" 358 | (* TODO for now, don't allow text objects *) 359 | text-radio-target = <'<<<'> text-target-name <'>>>'> 360 | 361 | (* https://orgmode.org/worg/dev/org-syntax.html#Macros 362 | https://orgmode.org/manual/Macro-Replacement.html 363 | *) 364 | text-macro = <'{{{'> macro-name [ <'('> macro-args <')'> ] <'}}}'> 365 | macro-name = #"[a-zA-Z][-\w]*" 366 | macro-args = <''> | macro-arg { <','> [s] macro-arg } 367 | (* "ARGUMENTS can contain anything but “}}}” string." 368 | Comma must be escaped; ")" is only allowed when not followed by "}}}". *) 369 | <macro-arg> = #"(\\,|[^)},]|\}(?!\}\})|\)(?!\}\}\}))*" 370 | 371 | (* Entities and LaTeX Fragments 372 | https://orgmode.org/worg/dev/org-syntax.html#Entities_and_LaTeX_Fragments 373 | https://orgmode.org/manual/LaTeX-fragments.html#LaTeX-fragments 374 | https://orgmode.org/manual/LaTeX-fragments.html#LaTeX-fragments 375 | The entity name must be in org-entities or org-entities-user. As 376 | this is configurable, we parse any name that looks valid. 377 | 378 | C-c C-x \ (org-toggle-pretty-entities) 379 | 380 | TODO Implement other latex syntax: 381 | \command[x]{y}{z}, \( \), \[ \], $$ $$, $ $ 382 | text-entity must be edited to also match \command[]{}s 383 | *) 384 | text-entity = "" 385 | text-entity = <'\\'> entity-name ( entity-braces | ε & <#"[^a-zA-Z]|$"> ) 386 | entity-name = #"[a-zA-Z]+" 387 | entity-braces = <'{}'> 388 | 389 | text-linebreak = <'\\\\'> text-linebreak-after 390 | text-linebreak-after = #"\s*$" 391 | 392 | (* Hyperlinks (regular links) 393 | https://orgmode.org/guide/Hyperlinks.html 394 | https://orgmode.org/manual/Link-Format.html 395 | 396 | URIs in text, optionally wrapped in <>, are recognized as links. 397 | The general link format is [[]] and [[][]]. 398 | 399 | *) 400 | 401 | (* Any text inside link brackets [[...][...]]. 402 | Backslash is the escape character for itself and opening/closing brackets. 403 | *) 404 | <link-inside> = #"(\\\[|\\\]|\\\\|[^\[\]])+" 405 | 406 | link-format = <'[['> link <']]'> / <'[['> link <']['> link-description <']]'> 407 | 408 | (* "If the link does not look like a URL, it is considered to be internal in the current file." 409 | - from orgmode guide. Hence the ordered alternatives: *) 410 | link = link-ext / link-int 411 | (* TODO does description must support markup? *) 412 | link-description = link-inside 413 | 414 | (* External Links 415 | https://orgmode.org/manual/External-Links.html 416 | https://orgmode.org/worg/dev/org-syntax.html#Links *) 417 | link-ext = link-ext-file / link-ext-id / link-ext-other 418 | 419 | (* TODO missing: ssh support *) 420 | link-ext-file = ( <'file:'> | & #"\.?/" ) link-inside-filename [ link-ext-file-location ] 421 | <link-inside-filename> = #"(\\\[|\\\]|\\\\|:(?!:)|[^:\[\]])+" 422 | <link-ext-file-location> = <'::'> ( link-file-loc-lnum / link-file-loc-headline / link-file-loc-customid / link-file-loc-string ) 423 | link-file-loc-lnum = #"\d+" 424 | link-file-loc-headline = <'*'> link-inside 425 | link-file-loc-customid = <'#'> link-inside 426 | link-file-loc-string = link-inside 427 | 428 | link-ext-id = <"id:"> #"[0-9a-fA-F-]+" 429 | 430 | link-ext-other = link-url-scheme <':'> link-url-rest 431 | (* see org-link-types-re *) 432 | link-url-scheme = #"[a-z][a-z0-9]+" 433 | link-url-rest = link-inside 434 | 435 | (* Internal Links 436 | https://orgmode.org/manual/Internal-Links.html 437 | 438 | Here, link-file-loc-string works different than in link-ext-file: 439 | It is not text search but a link to <<id>> or #+NAME: id. See manual. 440 | *) 441 | link-int = link-file-loc-headline / link-file-loc-customid / link-file-loc-string 442 | 443 | (* Tables 444 | https://orgmode.org/worg/dev/org-syntax.html#Tables 445 | https://orgmode.org/worg/org-tutorials/tables.html 446 | 447 | Two types of tables: org and table.el tables 448 | *) 449 | table = table-org / table-tableel 450 | table-org = table-row <eol> { table-row <eol> } { table-formula <eol> } 451 | table-tableel = table-tableel-sep <eol> { ( table-tableel-line / table-tableel-sep ) <eol> } 452 | 453 | table-tableel-sep = [s] #'\+-[+-]*' 454 | table-tableel-line = [s] #'\|[^\n]*' 455 | 456 | table-row = [s] ( table-row-sep / table-row-cells ) 457 | table-row-sep = #'\|-[-+|]*' 458 | table-row-cells = <'|'> table-cell { <'|'> table-cell } [ <'|'> ] 459 | table-cell = #'[^|\n]*' 460 | table-formula = [s] <'#+TBLFM: '> anything-but-newline 461 | 462 | 463 | (* Clock, Diary Sexp and Planning 464 | https://orgmode.org/worg/dev/org-syntax.html#Clock,_Diary_Sexp_and_Planning 465 | *) 466 | 467 | clock = [s] <'CLOCK:'> s ( timestamp-inactive-no-range / timestamp-inactive-range s <'=>'> s clock-duration ) [s] 468 | clock-duration = clock-dur-hh <':'> clock-dur-mm 469 | clock-dur-hh = #'\d+' 470 | clock-dur-mm = #'\d\d' 471 | 472 | diary-sexp = <'%%('> anything-but-newline 473 | 474 | (* Planning element must directly follow a headline. 475 | TODO change the headline to parse the next line as planning! *) 476 | planning = [s] planning-info { s planning-info } [s] 477 | planning-info = planning-keyword <':'> s timestamp 478 | planning-keyword = planning-kw-deadline / planning-kw-scheduled / planning-kw-closed 479 | planning-kw-deadline = <'DEADLINE'> 480 | planning-kw-scheduled = <'SCHEDULED'> 481 | planning-kw-closed = <'CLOSED'> 482 | -------------------------------------------------------------------------------- /src/org_parser/cli.clj: -------------------------------------------------------------------------------- 1 | (ns org-parser.cli 2 | (:gen-class) 3 | (:require [org-parser.core :as core] 4 | [org-parser.render :as render] 5 | [clojure.string :as str])) 6 | 7 | (defn -main [path & args] 8 | (->> path 9 | slurp 10 | core/read-str 11 | render/json 12 | str/trim-newline 13 | println)) 14 | -------------------------------------------------------------------------------- /src/org_parser/core.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.core 2 | #?(:clj (:gen-class)) 3 | (:require [org-parser.parser :as parser] 4 | [org-parser.transform :as transform] 5 | [org-parser.render :as render] 6 | [clojure.string :as string] 7 | )) 8 | 9 | (defn read-str 10 | "Reads one ORG value from input String. Takes optional Options." 11 | [string & options] 12 | (-> string 13 | parser/parse 14 | transform/transform)) 15 | 16 | #_(read-str "** headline _underlined_ / +strikethrough+ :tag:baz: \n foo/bar") 17 | #_(read-str "* headline/foo") 18 | #_(read-str "foo/bar") 19 | #_(read-str "{{{my( arg1 , {'arg 2' } )}}}") 20 | 21 | (defn write-str 22 | "Converts x to a ORG-formatted string. Takes optional Options." 23 | [x & options] 24 | (render/render x)) 25 | -------------------------------------------------------------------------------- /src/org_parser/macros.clj: -------------------------------------------------------------------------------- 1 | (ns org-parser.macros 2 | (:require [clojure.java.io :as io])) 3 | 4 | (defmacro inline [path] 5 | (slurp (io/resource path))) 6 | -------------------------------------------------------------------------------- /src/org_parser/parser.clj: -------------------------------------------------------------------------------- 1 | (ns org-parser.parser 2 | (:require [instaparse.core :as insta])) 3 | 4 | (def parser 5 | (-> "org.ebnf" 6 | clojure.java.io/resource 7 | insta/parser)) 8 | 9 | 10 | (defn parse [& args] 11 | (-> parser 12 | (apply args) 13 | (vary-meta merge {:raw (first args)}))) 14 | -------------------------------------------------------------------------------- /src/org_parser/parser.cljs: -------------------------------------------------------------------------------- 1 | (ns org-parser.parser 2 | (:require-macros [org-parser.macros :as macros]) 3 | (:require [instaparse.core :as insta :refer-macros [defparser]])) 4 | 5 | (defparser parser (macros/inline "org.ebnf")) 6 | 7 | (defn parse [& args] 8 | (-> (apply parser args) 9 | (vary-meta merge {:raw (first args)}))) 10 | -------------------------------------------------------------------------------- /src/org_parser/render.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.render 2 | (:require [clojure.string :as str] 3 | #?(:clj [clojure.data.json :as json]))) 4 | 5 | ;; See also export_org.js in https://github.com/200ok-ch/organice for inspirations 6 | 7 | 8 | ;; FIXME: delete the next 2 functions because it's not the 9 | ;; responsiblity of org-parser to render edn or json 10 | (defn edn [x] 11 | (prn-str x)) 12 | 13 | (defn json [x] 14 | #?(:clj (json/write-str x) 15 | :cljs (.stringify js/JSON (clj->js x)))) 16 | 17 | 18 | ;; TODO: This is a minimal implementation of rendering the 19 | ;; deserialized org data structure back to an org string. This needs 20 | ;; to be extenden to the full feature scope. 21 | 22 | (defn- serialize-text-element [[tag text]] 23 | (case tag 24 | :text-sty-bold (str "*" text "*") 25 | :text-sty-italic (str "/" text "/") 26 | :text-sty-underlined (str "_" text "_") 27 | :text-sty-strikethrough (str "+" text "+") 28 | :text-sty-verbatim (str "=" text "=") 29 | :text-sty-code (str "~" text "~") 30 | text)) 31 | 32 | (defn- serialize-text [elements] 33 | (apply str (map serialize-text-element elements))) 34 | 35 | #_(serialize-text [[:text-normal "hello "] [:text-bold "world"] [:asdf "!"]]) 36 | 37 | (defn- serialize-headline* [headline] 38 | (str/join " " 39 | [(apply str (repeat (:level headline) "*")) 40 | (serialize-text (:title headline))])) 41 | 42 | (defn- serialize-section [{:keys [ast]}] 43 | ast) 44 | 45 | (defn- serialize-headline [{:keys [headline section]}] 46 | (str/join "\n" 47 | [(serialize-headline* headline) 48 | (serialize-section section)])) 49 | 50 | (defn render [{:keys [settings preamble headlines]}] 51 | (str/join "\n" 52 | (remove nil? 53 | (cons 54 | ;; TODO: serialize settings 55 | (serialize-section (:section preamble)) 56 | (map serialize-headline headlines))))) 57 | 58 | 59 | #_(render {:headlines [{:headline {:level 1 :title "foo"}}]}) 60 | -------------------------------------------------------------------------------- /src/org_parser/transform.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.transform 2 | (:require [clojure.string :as str] 3 | [instaparse.core :as insta])) 4 | 5 | ;; See also parse_org.js in https://github.com/200ok-ch/organice for inspirations 6 | 7 | (def conjv (comp vec conj)) 8 | 9 | (def consv (comp vec cons)) 10 | 11 | 12 | (defmulti reducer 13 | "The reducer multi method takes a RESULT and an AST and dispatches 14 | on the first element in AST, which is the type (a keyword) of the 15 | parsed line, e.g. `:headline`, `:content-line`, etc." 16 | (fn [_ line _] (first line))) 17 | 18 | 19 | (defn- append-to-document [state ast raw] 20 | (let [loc (if-let [headlines (state :headlines)] 21 | [:headlines (dec (count headlines)) :section] 22 | [:preamble :section])] 23 | (-> state 24 | ;; append line to :ast 25 | (update-in (conj loc :ast) conjv ast) 26 | ;; append raw string to :raw 27 | (update-in (conj loc :raw) conjv raw)))) 28 | 29 | 30 | ;; add the given ast to the section of the last headline or to the 31 | ;; preamble if there are no headlines yet 32 | (defmethod reducer :default [state ast raw] 33 | (append-to-document state ast raw)) 34 | 35 | 36 | #_(transform (org-parser.parser/parse "* hello\n** world\n\nasdf")) 37 | 38 | 39 | (defn- property-node 40 | "Takes a PROP (a keyword) and a seq PROPS. Finds the occurence of 41 | PROP in PROPS and returns the node." 42 | [prop props] 43 | (->> props 44 | (filter #(= prop (first %))) 45 | first)) 46 | 47 | (defn- property 48 | "Takes a PROP (a keyword) and a seq PROPS. Finds the occurence of 49 | PROP in PROPS and returns a seq of its values." 50 | [prop props] 51 | (->> props 52 | (property-node prop) 53 | (drop 1) 54 | vec)) 55 | 56 | ;; Unused? 57 | (defn- replace-first-property [elements prop f] 58 | "In a vector like [[:a 1] [:b 2]], replace the first matching tagged 59 | list with a new tagged list using the mapper function f." 60 | (let [head (take-while #(not= (first %) prop) elements) 61 | tail (drop-while #(not= (first %) prop) elements)] 62 | (concat head [(f (first tail))] (drop 1 tail)))) 63 | 64 | (comment 65 | (replace-first-property [[:a 1] [:b 2] [:c 3] [:d 4]] :b identity) 66 | (replace-first-property [[:a 1] [:b 2] [:c 3] [:d 4]] :b (fn [_] [:b 100])) 67 | (vec [:a 1]) 68 | (concat [1 2 3] [4] [5])) 69 | 70 | (defn- extract-tags [[_ s]] 71 | "Given a [:text-normal 'xxx'], return the text-normal without tags 72 | and a vector of tags." 73 | (let [[tags & _] (re-find #"\s+(:[a-zA-Z0-9_@#%]+)+:\s*$" s)] ;; find tags by regex 74 | (if (nil? tags) 75 | [[:text-normal s] []] 76 | [[:text-normal (subs s 0 (- (count s) (count tags)))] 77 | (vec (filter #(not (= % "")) (str/split (str/trim tags) #":" )))]))) 78 | 79 | (comment 80 | (extract-tags [:text-normal "title :tag1:tag2:"]) 81 | (str/split (->> " :tag1:tag2: " str/trim) #":") 82 | (let [[x & _] nil] x) 83 | (re-find #"\s+(:[a-zA-Z0-9_@#%]+)+:\s*$" "title :tag:tag:")) 84 | 85 | (defn- extract-tags-from-text [texts] 86 | (let [lasttext (last texts)] 87 | (if (= (first lasttext) :text-normal) 88 | (let [[text tags] (extract-tags lasttext)] 89 | [(conjv (vec (butlast texts)) text) tags]) 90 | [texts []]))) 91 | 92 | #_(extract-tags-from-text [[:text-bold "bold"] [:text-x "foo"] [:text-normal "und :tag:"]]) 93 | 94 | (defmethod reducer :headline [state [_ & properties] raw] 95 | (let [[title tags] (->> properties (property :text) extract-tags-from-text)] 96 | (update state :headlines 97 | conjv {:headline {:level (->> properties (property :level) first) 98 | :title title 99 | :planning (->> properties (property :planning)) 100 | :keyword (->> properties 101 | (property :keyword) 102 | first) 103 | :priority (->> properties 104 | (property :priority) 105 | first) 106 | :commented? (->> properties 107 | (property-node :comment-token) 108 | (seq) 109 | (boolean)) 110 | :tags tags}}))) 111 | 112 | 113 | ;; content-line needs to simply drop the keyword 114 | (defmethod reducer :content-line [state [_ ast] raw] 115 | (append-to-document state ast raw)) 116 | 117 | 118 | (defn- text-reducer [accu element] 119 | (case accu 120 | [] [element] 121 | (let [[lastkey lastval] (last accu) 122 | [newkey newval] element] 123 | (if (and (= lastkey newkey) (= newkey :text-normal)) 124 | (conjv (vec (butlast accu)) [newkey (str lastval newval)]) ;; WTF?! without vec in (vec (butlast .)) the output is total crap 125 | (conjv accu element))))) 126 | 127 | #_(reduce text-reducer [] [[:text-underlined "underlined"] 128 | [:text-normal "a"] 129 | [:text-normal "/"]]) 130 | 131 | #_(reduce text-reducer [] [[:text-normal "asdf"] [:text-normal "jklö"] [:text-bold "test"]]) 132 | #_(reduce text-reducer [] [[:text-normal "z"] [:text-normal "a"] [:text-bold "test"] [:text-normal "0"]]) 133 | #_((let [[lastkey lastval] (last [])] [lastkey lastval])) 134 | #_((let [x (last [])] x)) 135 | 136 | #_(text-reducer [] [:text-normal "asdf"]) 137 | #_(text-reducer [[:text-normal "asdf"]] [:text-normal "jklö"]) 138 | #_(text-reducer [[:text-bold "asdf"]] [:text-normal "jklö"]) 139 | #_(text-reducer [[:text-normal "asdf"]] [:text-bold "jklö"]) 140 | 141 | (defn- merge-consecutive-text-normal [& elements] 142 | "Merge consecutive :text-normal inside a :text list. They come from 143 | the parser stopping at any special character like '*', '/', ..." 144 | (vec (concat [:text] (reduce text-reducer [] elements)))) 145 | 146 | #_(apply merge-consecutive-text-normal [[:text-normal "asdf"] [:text-normal "jklö"] [:text-bold "test"]]) 147 | #_(apply merge-consecutive-text-normal [[:text-normal "foo "] [:text-normal "bar"] [:text-sty-bold "bar"] [:text-normal " baz"]]) 148 | 149 | (defn- wrap-raw [reducer raw] 150 | (fn [agg ast] 151 | (reducer agg ast (apply subs raw (insta/span ast))))) 152 | 153 | (defn transform [x] 154 | (->> x 155 | (insta/transform 156 | {:text merge-consecutive-text-normal 157 | :title merge-consecutive-text-normal ;; :title just a synonym for :text in a headline 158 | :stars #(vector :level (count %)) 159 | :timestamp identity 160 | :macro-args #(vector :macro-args (map str/trim %&)) 161 | }) 162 | (drop 1) ;; drops the initial `:S` 163 | (reduce (wrap-raw reducer (-> x meta :raw)) {}))) 164 | -------------------------------------------------------------------------------- /test/org_parser/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.core-test 2 | (:require #?(:clj [clojure.test :refer :all] 3 | :cljs [cljs.test :refer-macros [deftest is testing]]) 4 | #?(:cljs [cljs-node-io.core :refer [slurp]]) 5 | [org-parser.core :as core])) 6 | 7 | (deftest full-round-trip 8 | (testing "minimal" 9 | (let [minimal (slurp "test/org_parser/fixtures/minimal.org")] 10 | (is (= minimal (-> minimal core/read-str core/write-str)))))) 11 | 12 | 13 | (deftest headline-data 14 | (is (= (core/read-str "* foo bar") 15 | {:headlines [{:headline {:level 1, 16 | :title [[:text-normal "foo bar"]], 17 | :planning [], 18 | :keyword nil, 19 | :priority nil, 20 | :commented? false 21 | :tags []}}]})) 22 | (is (= (core/read-str "* foo bar :a:b:") 23 | {:headlines [{:headline 24 | {:level 1, 25 | :title [[:text-normal "foo bar"]], 26 | :planning [], 27 | :keyword nil, 28 | :priority nil, 29 | :commented? false 30 | :tags ["a" "b"]}}]})) 31 | (is (= (core/read-str "* TODO foo bar") 32 | {:headlines [{:headline 33 | {:level 1, 34 | :title [[:text-normal "foo bar"]], 35 | :planning [], 36 | :keyword "TODO", 37 | :priority nil, 38 | :commented? false 39 | :tags []}}]})) 40 | (is (= (core/read-str "* COMMENT foo bar") 41 | {:headlines [{:headline 42 | {:level 1, 43 | :title [[:text-normal "foo bar"]], 44 | :planning [], 45 | :keyword nil, 46 | :priority nil, 47 | :commented? true 48 | :tags []}}]})) 49 | (is (= (core/read-str "* TODO COMMENT foo bar") 50 | {:headlines [{:headline 51 | {:level 1, 52 | :title [[:text-normal "foo bar"]], 53 | :planning [], 54 | :keyword "TODO", 55 | :priority nil, 56 | :commented? true 57 | :tags []}}]})) 58 | (is (= (core/read-str "* [#B] foo bar") 59 | {:headlines [{:headline {:level 1, 60 | :title [[:text-normal "foo bar"]], 61 | :planning [], 62 | :keyword nil, 63 | :priority "B", 64 | :commented? false 65 | :tags []}}]})) 66 | (is (= (core/read-str "* TODO [#B] foo bar") 67 | {:headlines [{:headline {:level 1, 68 | :title [[:text-normal "foo bar"]], 69 | :planning [], 70 | :keyword "TODO", 71 | :priority "B", 72 | :commented? false 73 | :tags []}}]}))) 74 | -------------------------------------------------------------------------------- /test/org_parser/fixtures/bold_text.org: -------------------------------------------------------------------------------- 1 | * Main header (required for parser atm) 2 | *This is bold*: Hello spec! 3 | 4 | *Bold* text can also be just one word. 5 | 6 | *This is also bold*: And here goes for some more text which even 7 | includes more *bold* statements. 8 | -------------------------------------------------------------------------------- /test/org_parser/fixtures/headlines_and_tables.org: -------------------------------------------------------------------------------- 1 | * Headline 1 2 | 3 | | first column 1 | first column 2 | 4 | | first value 1 | first value 2 | 5 | 6 | | second column 1 | second column 2 | 7 | | second value 1 | second value 2 | 8 | 9 | * Headline 2 10 | 11 | | people | age | 12 | |------------+-----| 13 | | bob | 38 | 14 | | max | 42 | 15 | |------------+-----| 16 | | median age | 40 | 17 | #+TBLFM: @4$2=vmean(@2..@-1) 18 | 19 | * table.el style table 20 | 21 | The option to use org tables and table.el tables is documented in 22 | the spec: https://orgmode.org/worg/dev/org-syntax.html#Tables 23 | 24 | Hence, =org-parser= should and does parse it! 25 | 26 | +-----+-----+ 27 | | people | age | 28 | +-----+-----+ 29 | | bob | 38 | 30 | +-----+-----+ 31 | | max | 42 | 32 | +-----+-----+ 33 | -------------------------------------------------------------------------------- /test/org_parser/fixtures/minimal.org: -------------------------------------------------------------------------------- 1 | * Hello World 2 | -------------------------------------------------------------------------------- /test/org_parser/fixtures/schedule_with_repeater.org: -------------------------------------------------------------------------------- 1 | * TODO *Header* with repeater 2 | SCHEDULED: <2019-11-27 Wed +1d> 3 | -------------------------------------------------------------------------------- /test/org_parser/integration/section_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.integration.section-test 2 | (:require [clojure.string :as str] 3 | [org-parser.parser :as parser] 4 | [org-parser.transform :as transform] 5 | #?(:clj [clojure.test :refer :all] 6 | :cljs [cljs.test :refer-macros [deftest is testing]]))) 7 | 8 | 9 | (defn- t [& l] (str/join "\n" l)) 10 | 11 | 12 | (def samples 13 | [{:input 14 | (t "* hello world" 15 | "this is the first section" 16 | "this line has *bold text*") 17 | 18 | :ast 19 | [:S 20 | [:headline [:stars "*"] [:text [:text-normal "hello world"]]] 21 | [:content-line [:text [:text-normal "this is the first section"]]] 22 | [:content-line 23 | [:text 24 | [:text-normal "this line has "] 25 | [:text-sty-bold "bold text"]]]] 26 | 27 | :result 28 | {:headlines 29 | [{:headline 30 | {:level 1, 31 | :title [[:text-normal "hello world"]], 32 | :planning [], 33 | :keyword nil 34 | :priority nil 35 | :commented? false 36 | :tags []}, 37 | :section 38 | {:ast 39 | [[:text [:text-normal "this is the first section"]] 40 | [:text 41 | [:text-normal "this line has "] 42 | [:text-sty-bold "bold text"]]], 43 | :raw ["this is the first section" "this line has *bold text*"]}}]}} 44 | 45 | ;; next sample here: 46 | ;; {:input ... 47 | ;; :ast ... 48 | ;; :result ...} 49 | ]) 50 | 51 | 52 | (deftest section 53 | (doseq [{:keys [input ast result output]} samples] 54 | (let [ast* (parser/parse input)] 55 | (testing "AST matches expected AST" (is (= ast ast*))) 56 | (testing "transformed AST matches expected transformed AST" (is (= result (transform/transform ast*))))))) 57 | 58 | 59 | #_(-> samples first :input parser/parse transform/transform) 60 | -------------------------------------------------------------------------------- /test/org_parser/parser_mean_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.parser-mean-test 2 | (:require [org-parser.parser :as parser] 3 | #?(:clj [clojure.test :refer :all] 4 | :cljs [cljs.test :refer-macros [deftest is testing]]))) 5 | 6 | (deftest headline 7 | (let [parse #(parser/parse % :start :headline)] 8 | (testing "with crazy characters in title" 9 | (is (= [:headline [:stars "*****"] [:text [:text-normal "hello wörld⛄ :"]]] 10 | (parse "***** hello wörld⛄ :")))))) 11 | -------------------------------------------------------------------------------- /test/org_parser/parser_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.parser-test 2 | (:refer-clojure :exclude [keyword]) 3 | (:require [org-parser.parser :as parser] 4 | [instaparse.core :as insta] 5 | #?(:clj [clojure.test :refer :all]) 6 | #?(:cljs [cljs.test :refer-macros [deftest is testing]]) 7 | #?(:cljs [cljs-node-io.core :refer [slurp]]))) 8 | 9 | 10 | ;; if parse is successful it returns a vector otherwise a map 11 | 12 | (deftest check-regex-syntax 13 | ;; There are so many dialects of regex. AFAIK, instaparse uses Java/Clojure regex syntax. 14 | ;; To be sure, here are some checks: 15 | (testing "escaping of '-' within brackets work" 16 | ;; in other dialects, the regex would be written like: [- X] 17 | (is (re-matches #"[ \-X]" "-"))) 18 | (testing "In [.], . doesn't have to be escaped" ;; just to be sure 19 | (is (not (re-matches #"[.]" "x")))) 20 | (testing ". does not match newline" 21 | (is (not (re-matches #"." "\n")))) 22 | (testing ". does not match carriage return" 23 | (is (not (re-matches #"." "\r")))) 24 | (testing "[^x] matches newline" 25 | (is (re-matches #"[^x]" "\n"))) 26 | (testing "\\s does match newline" 27 | (is (re-matches #"\s" "\n"))) 28 | (testing "\\s does match carriage return" 29 | (is (re-matches #"\s" "\r"))) 30 | (testing "\\w does not match unicode letters" 31 | (is (not (re-matches #"\w*" "abökoß")))) 32 | ;; The cljs build does not work with this Java Unicode regex pattern :( 33 | ;; (testing "\\p{L} does match unicode letters" 34 | ;; (is (re-matches #"\p{L}*" "abökoßα"))) 35 | ) 36 | 37 | 38 | 39 | (deftest basic-terminals 40 | (testing "newline as <eol>" 41 | (is (= () (#(parser/parse % :start :eol) "\n")))) 42 | (testing "carriage return as <eol>" 43 | (is (= () (#(parser/parse % :start :eol) "\r")))) 44 | (testing "horizontal space <s> does not match form feed" 45 | (is (insta/failure? (#(parser/parse % :start :s) "\f")))) 46 | (testing "horizontal space <s> does not match CR" 47 | (is (insta/failure? (#(parser/parse % :start :s) "\r")))) 48 | ) 49 | 50 | (deftest word 51 | (let [parse #(parser/parse % :start :word)] 52 | (testing "single" 53 | (is (= ["a"] 54 | (parse "a")))) 55 | (testing "single with trailing space" 56 | (is (insta/failure? (parse "ab ")))) 57 | (testing "single with trailing newline" 58 | (is (insta/failure? (parse "a\n")))))) 59 | 60 | 61 | ;; NOTE: Tags are defined in EBNF but currently not effective. Tags 62 | ;; are extracted in the transform phase. 63 | (deftest tags 64 | (let [parse #(parser/parse % :start :tags)] 65 | (testing "single" 66 | (is (= [:tags "a"] 67 | (parse ":a:")))) 68 | (testing "single underscore" 69 | (is (= [:tags "_"] 70 | (parse ":_:")))) 71 | (testing "multiple" 72 | (is (= [:tags "a" "b" "c"] 73 | (parse ":a:b:c:")))) 74 | (testing "with all edge characters" 75 | (is (= [:tags "az" "AZ" "09" "_@#%"] 76 | (parse ":az:AZ:09:_@#%:")))))) 77 | 78 | 79 | (deftest headline 80 | (let [parse #(parser/parse % :start :headline)] 81 | (testing "boring" 82 | (is (= [:headline [:stars "*"] [:text [:text-normal "hello world"]]] 83 | (parse "* hello world")))) 84 | (testing "with priority" 85 | (is (= [:headline [:stars "**"] [:priority "A"] [:text [:text-normal "hello world"]]] 86 | (parse "** [#A] hello world")))) 87 | (testing "with tags" 88 | (is (= [:headline [:stars "***"] [:text [:text-normal "hello world :the:end:"]]] 89 | (parse "*** hello world :the:end:")))) 90 | (testing "with priority and tags" 91 | (is (= [:headline [:stars "****"] [:priority "B"] [:text [:text-normal "hello world :the:end:"]]] 92 | (parse "**** [#B] hello world :the:end:")))) 93 | (testing "title cannot have multiple lines" 94 | (is (insta/failure? (parse "* a\nb")))) 95 | (testing "with todo keyword" 96 | (is (= [:headline [:stars "*"] [:keyword "TODO"] [:text [:text-normal "hello world"]]] 97 | (parse "* TODO hello world")))) 98 | (testing "with todo keyword and comment flag" 99 | (is (= [:headline [:stars "*"] [:keyword "TODO"] [:comment-token] [:text [:text-normal "hello world"]]] 100 | (parse "* TODO COMMENT hello world")))) 101 | (testing "with comment flag but without todo keyword or prio: interpret COMMENT as keyword" 102 | (is (= [:headline [:stars "*****"] [:comment-token] [:text [:text-normal "hello world"]]] 103 | (parse "***** COMMENT hello world")))) 104 | (testing "headline with planning info in next line" 105 | (is (= [:headline [:stars "*"] [:text [:text-normal "hello"]] 106 | [:planning 107 | [:planning-info 108 | [:planning-keyword [:planning-kw-closed]] 109 | [:timestamp [:timestamp-inactive [:ts-inner [:ts-inner-wo-time [:ts-date "2021-05-22"] [:ts-day "Sat"]] [:ts-modifiers]]]]]]] 110 | (parse "* hello\n CLOSED: [2021-05-22 Sat]")))) 111 | )) 112 | 113 | 114 | (deftest line 115 | (let [parse #(parser/parse % :start :line)] 116 | (testing "horizontal rule" 117 | (is (= [[:horizontal-rule "-----"]] 118 | (parse "-----")))) 119 | (testing "horizontal rule space-indented" 120 | (is (= [[:horizontal-rule " --------"]] 121 | (parse " --------")))) 122 | 123 | (testing "keyword line" 124 | (is (= [[:other-keyword-line [:kw-name "KEY"] [:kw-value "VALUE"]]] 125 | (parse "#+KEY: VALUE")))) 126 | 127 | (testing "comment line" 128 | (is (= [[:comment-line [:comment-line-head "#"] [:comment-line-rest ""]]] 129 | (parse "#")))) 130 | (testing "comment line" 131 | (is (= [[:comment-line [:comment-line-head "#"] [:comment-line-rest " "]]] 132 | (parse "# ")))) 133 | (testing "comment line" 134 | (is (= [[:comment-line [:comment-line-head "#"] [:comment-line-rest " comment"]]] 135 | (parse "# comment")))) 136 | (testing "comment line" 137 | (is (= [[:comment-line [:comment-line-head "\t#"] [:comment-line-rest " comment"]]] 138 | (parse "\t# comment")))) 139 | )) 140 | 141 | (deftest comment-line 142 | (let [parse #(parser/parse % :start :comment-line)] 143 | (testing "no valid comment line" 144 | (is (insta/failure? (parse "#comment")))) 145 | (testing "no valid comment line" 146 | (is (insta/failure? (parse "#\tcomment")))))) 147 | 148 | ;; (deftest content 149 | ;; (let [parse #(parser/parse % :start :content-line)] 150 | ;; (testing "boring" 151 | ;; (is (= [[:content-line "anything"] 152 | ;; [:content-line "goes"]] 153 | ;; (parse "anything\ngoes")))))) 154 | 155 | 156 | (deftest sections 157 | (let [parse parser/parse] 158 | (testing "boring org file" 159 | (is (= [:S 160 | [:headline [:stars "*"] [:text [:text-normal "hello world"]]] 161 | [:content-line [:text [:text-normal "this is the first section"]]] 162 | [:headline [:stars "**"] [:text [:text-normal "and this"]]] 163 | [:content-line [:text [:text-normal "is another section"]]]] 164 | (parse "* hello world 165 | this is the first section 166 | ** and this 167 | is another section")))) 168 | (testing "boring org file with empty lines" 169 | (is (=[:S 170 | [:headline [:stars "*"] [:text [:text-normal "hello world"]]] 171 | [:content-line [:text [:text-normal "this is the first section"]]] 172 | [:empty-line] 173 | [:headline [:stars "**"] [:text [:text-normal "and this"]]] 174 | [:empty-line] 175 | [:content-line [:text [:text-normal "is another section"]]]] 176 | (parse "* hello world 177 | this is the first section 178 | 179 | ** and this 180 | 181 | is another section")))))) 182 | 183 | 184 | (deftest affiliated-keyword 185 | (let [parse #(parser/parse % :start :affiliated-keyword-line)] 186 | (testing "header" 187 | (is (= [:affiliated-keyword-line [:affil-kw-key "HEADER"] [:kw-value "hello world"]] 188 | (parse "#+HEADER: hello world")))) 189 | (testing "name" 190 | (is (= [:affiliated-keyword-line [:affil-kw-key "NAME"] [:kw-value "hello world"]] 191 | (parse "#+NAME: hello world")))) 192 | (testing "PLOT" 193 | (is (= [:affiliated-keyword-line [:affil-kw-key "PLOT"] [:kw-value "hello world"]] 194 | (parse "#+PLOT: hello world")))) 195 | (testing "results" 196 | (is (= [:affiliated-keyword-line [:affil-kw-key "RESULTS"] [:kw-value "hello world"]] 197 | (parse "#+RESULTS: hello world")))) 198 | (testing "results" 199 | (is (= [:affiliated-keyword-line [:affil-kw-key "RESULTS" [:affil-kw-optional "asdf"]] [:kw-value "hello world"]] 200 | (parse "#+RESULTS[asdf]: hello world")))) 201 | (testing "caption" 202 | (is (= [:affiliated-keyword-line [:affil-kw-key "CAPTION"] [:kw-value "hello world"]] 203 | (parse "#+CAPTION: hello world")))) 204 | (testing "caption" 205 | (is (= [:affiliated-keyword-line [:affil-kw-key "CAPTION" [:affil-kw-optional "qwerty"]] [:kw-value "hello world"]] 206 | (parse "#+CAPTION[qwerty]: hello world")))))) 207 | 208 | 209 | ;; this is a special case of in-buffer-settings 210 | (deftest todo 211 | (let [parse #(parser/parse % :start :todo-line)] 212 | (testing "todos" 213 | (is (= [:todo-line [:todo-state "TODO"] [:done-state "DONE"]] 214 | (parse "#+TODO: TODO | DONE")))))) 215 | 216 | (deftest blocks 217 | (let [parse #(parser/parse % :start :block)] 218 | (testing "no content" 219 | (is (= [:block [:greater-block 220 | [:block-begin-line [:block-name "center"] [:block-parameters "params! "]] 221 | [:block-end-line [:block-name "center"]]]] 222 | (parse "#+BEGIN_center params! \n#+end_center")))) 223 | (testing "one line of content" 224 | (is (= [:block [:greater-block 225 | [:block-begin-line [:block-name "QUOTE"]] 226 | [:content-line [:text [:text-normal "content"]]] 227 | [:block-end-line [:block-name "QUOTE"]]]] 228 | (parse "#+BEGIN_QUOTE \ncontent\n#+end_QUOTE ")))) 229 | (testing "more lines of content" 230 | (is (= [:block [:greater-block 231 | [:block-begin-line [:block-name "center"]] 232 | [:content-line [:text [:text-normal "my"]]] 233 | [:content-line [:text [:text-normal "content"]]] 234 | [:block-end-line [:block-name "center"]]]] 235 | (parse "#+BEGIN_center\nmy\ncontent\n#+end_center")))) 236 | (testing "parse even if block name at begin and end not matching" 237 | ;; This must be handled by in a later step. 238 | (is (= [:block [:greater-block 239 | [:block-begin-line [:block-name "one"]] 240 | [:block-end-line [:block-name "other"]]]] 241 | (parse "#+BEGIN_one\n#+end_other")))) 242 | )) 243 | 244 | (deftest noparse-blocks-alone 245 | ;; The parsing of multi-line content with the look-ahead regex wasn't easy... 246 | (let [parse #(parser/parse % :start :noparse-block)] 247 | (testing "no content" 248 | (is (= [:noparse-block 249 | [:noparse-block-begin-line [:block-name-noparse "src"]] 250 | [:noparse-block-content ""] 251 | [:block-end-line [:block-name "src"]]] 252 | (parse "#+BEGIN_src\n#+END_src")))) 253 | (testing "only one blank line" 254 | (is (= [:noparse-block 255 | [:noparse-block-begin-line [:block-name-noparse "src"]] 256 | [:noparse-block-content "\n"] 257 | [:block-end-line [:block-name "src"]]] 258 | (parse "#+BEGIN_src\n\n#+END_src")))) 259 | (testing "only one line of content" 260 | (is (= [:noparse-block 261 | [:noparse-block-begin-line [:block-name-noparse "src"]] 262 | [:noparse-block-content "content\n"] 263 | [:block-end-line [:block-name "src"]]] 264 | (parse "#+BEGIN_src\ncontent\n #+END_src")))) 265 | (testing "two lines of content" 266 | (is (= [:noparse-block 267 | [:noparse-block-begin-line [:block-name-noparse "src"]] 268 | [:noparse-block-content "content\n second line \n"] 269 | [:block-end-line [:block-name "src"]]] 270 | (parse "#+BEGIN_src\ncontent\n second line \n #+END_src")))) 271 | )) 272 | 273 | (deftest noparse-blocks 274 | (let [parse #(parser/parse % :start :block)] 275 | (testing "no content" 276 | (is (= [:block [:noparse-block 277 | [:noparse-block-begin-line [:block-name-noparse "example"] [:block-parameters "params! "]] 278 | [:noparse-block-content ""] 279 | [:block-end-line [:block-name "example"]]]] 280 | (parse "#+BEGIN_example params! \n#+end_example")))) 281 | (testing "one line of content" 282 | (is (= [:block [:noparse-block [:noparse-block-begin-line [:block-name-noparse "src"]] 283 | [:noparse-block-content "content\n"] 284 | [:block-end-line [:block-name "src"]]]] 285 | (parse "#+BEGIN_src \ncontent\n#+end_src ")))) 286 | (testing "more lines of content" 287 | (is (= [:block [:noparse-block [:noparse-block-begin-line [:block-name-noparse "export"]] 288 | [:noparse-block-content "my\ncontent\n"] 289 | [:block-end-line [:block-name "export"]]]] 290 | (parse "#+BEGIN_export\nmy\ncontent\n#+end_export")))) 291 | (testing "parse even if block name at begin and end not matching" 292 | ;; This must be handled by in a later step. 293 | (is (= [:block [:noparse-block 294 | [:noparse-block-begin-line [:block-name-noparse "comment"]] 295 | [:noparse-block-content ""] 296 | [:block-end-line [:block-name "other"]]]] 297 | (parse "#+BEGIN_comment\n#+end_other")))) 298 | )) 299 | 300 | (deftest block-begin 301 | (let [parse #(parser/parse % :start :block-begin-line)] 302 | (testing "block-begin" 303 | (is (= [:block-begin-line [:block-name "CENTER"] [:block-parameters "some params"]] 304 | (parse "#+BEGIN_CENTER some params")))))) 305 | 306 | (deftest block-end 307 | (let [parse #(parser/parse % :start :block-end-line)] 308 | (testing "block-end" 309 | (is (= [:block-end-line [:block-name "CENTER"]] 310 | (parse "#+END_CENTER")))))) 311 | 312 | (deftest dynamic-block 313 | (let [parse #(parser/parse % :start :dynamic-block)] 314 | (testing "no content" 315 | (is (= [:dynamic-block [:dynamic-block-begin-line 316 | [:dynamic-block-name "na.me"] 317 | [:dynamic-block-parameters "pa rams "]]] 318 | (parse "#+BEGIN: na.me pa rams \n#+end:")))) 319 | (testing "one line of content" 320 | (is (= [:dynamic-block [:dynamic-block-begin-line [:dynamic-block-name "name"]] 321 | [:content-line [:text [:text-normal "text"]]]] 322 | (parse "#+BEGIN: name \ntext\n#+end: ")))) 323 | ;; TODO doesn't work yet :( 324 | ;; (testing "parse reluctantly" 325 | ;; (is (insta/failure? (parse "#+BEGIN: name \n#+end:\n#+end:")))) 326 | (testing "content" 327 | (is (= [:dynamic-block [:dynamic-block-begin-line [:dynamic-block-name "abc"]] 328 | [:content-line [:text [:text-normal "multi"]]] 329 | [:content-line [:text [:text-normal "line"]]] 330 | [:content-line [:text [:text-normal "content"]]]] 331 | (parse "#+begin: abc \nmulti\nline\ncontent\n#+end: ")))))) 332 | 333 | 334 | 335 | (deftest drawer-begin 336 | (let [parse #(parser/parse % :start :drawer-begin-line)] 337 | (testing "drawer-begin" 338 | (is (= [:drawer-begin-line [:drawer-name "SOMENAME"]] 339 | (parse ":SOMENAME:")))))) 340 | 341 | (deftest drawer-end 342 | (let [parse #(parser/parse % :start :drawer-end-line)] 343 | (testing "drawer-end" 344 | (is (= [:drawer-end-line] 345 | (parse ":END:")))))) 346 | 347 | (deftest drawer 348 | (testing "simple drawer" 349 | (is (= [:S [:drawer-begin-line [:drawer-name "SOMENAME"]] [:drawer-end-line]] 350 | (parser/parse ":SOMENAME: 351 | :END:")))) 352 | (testing "drawer with a bit of content" 353 | (is (= [:S 354 | [:drawer-begin-line [:drawer-name "PROPERTIES"]] 355 | [:content-line [:text [:text-normal ":foo: bar"]]] 356 | [:drawer-end-line]] 357 | (parser/parse ":PROPERTIES:\n:foo: bar\n:END:"))))) 358 | 359 | (deftest drawer-semantic-block 360 | (let [parse #(parser/parse % :start :drawer)] 361 | (testing "drawer" 362 | (is (= [:drawer [:drawer-begin-line [:drawer-name "MYDRAWER"]] 363 | [:content-line [:text [:text-normal "any"]]] 364 | [:content-line [:text [:text-normal "text"]]]] 365 | (parse ":MYDRAWER:\nany\ntext\n:END:")))))) 366 | 367 | (deftest property-drawer-semantic-block 368 | (let [parse #(parser/parse % :start :property-drawer)] 369 | (testing "no properties" 370 | (is (= [:property-drawer] 371 | (parse ":PROPERTIES:\n:END:")))) 372 | (testing "one property" 373 | (is (= [:property-drawer [:node-property-line 374 | [:node-property-name "text"] 375 | [:node-property-plus] 376 | [:node-property-value [:text [:text-normal "my value"]]]]] 377 | (parse ":PROPERTIES:\n:text+: my value\n:END:")))) 378 | (testing "more properties" 379 | (is (= [:property-drawer 380 | [:node-property-line 381 | [:node-property-name "text"] 382 | [:node-property-plus] 383 | [:node-property-value [:text [:text-normal "my value"]]]] 384 | [:node-property-line 385 | [:node-property-name "PRO"] 386 | [:node-property-value [:text [:text-normal "abc"]]]]] 387 | (parse ":PROPERTIES:\n:text+: my value\n:PRO: abc\n:END:")))) 388 | (testing "can only contain properties" 389 | (is (insta/failure? (parse ":PROPERTIES:\ntext\n:END:")))) 390 | )) 391 | 392 | (deftest dynamic-block-begin 393 | (let [parse #(parser/parse % :start :dynamic-block-begin-line)] 394 | (testing "dynamic-block-begin" 395 | (is (= [:dynamic-block-begin-line [:dynamic-block-name "SOMENAME"] [:dynamic-block-parameters "some params"]] 396 | (parse "#+BEGIN: SOMENAME some params")))))) 397 | 398 | 399 | (deftest dynamic-block-end 400 | (let [parse #(parser/parse % :start :dynamic-block-end-line)] 401 | (testing "dynamic-block-end" 402 | (is (= [:dynamic-block-end-line] 403 | (parse "#+END:")))))) 404 | 405 | 406 | (deftest footnote-line 407 | (let [parse #(parser/parse % :start :footnote-line)] 408 | (testing "footnote with fn label" 409 | (is (= [:footnote-line [:fn-label "some-label"] [:text [:text-normal "some contents"]]] 410 | (parse "[fn:some-label] some contents")))) 411 | (testing "footnote with number label" 412 | (is (= [:footnote-line [:fn-label "123"] [:text [:text-normal "some contents"]]] 413 | (parse "[fn:123] some contents")))) 414 | (testing "invalid footnote with only a number; sorry, this is not a footnote in orgmode" 415 | (is (insta/failure? (parse "[123] some contents")))) 416 | )) 417 | 418 | (deftest footnote-link 419 | (let [parse #(parser/parse % :start :footnote-link)] 420 | ;; TODO styled text inside footnote-link is not yet possible because text parses the closing bracket ] 421 | (testing "footnote link with label" 422 | (is (= [:footnote-link [:fn-label "123"]] 423 | (parse "[fn:123]")))) 424 | (testing "footnote link with label" 425 | (is (= [:footnote-link "some contents"] 426 | (parse "[fn::some contents]")))) 427 | (testing "footnote link with label and text" 428 | (is (= [:footnote-link [:fn-label "some-label"] "some contents"] 429 | (parse "[fn:some-label:some contents]")))) 430 | (testing "footnote link with number and text" 431 | (is (= [:footnote-link [:fn-label "123"] "some contents"] 432 | (parse "[fn:123:some contents]")))) 433 | (testing "footnote link with label and invalid text" 434 | (is (insta/failure? (parse "[fn:some-label:some [contents]")))) 435 | (testing "footnote link with label and invalid text" 436 | (is (insta/failure? (parse "[fn:some-label:some ]contents]")))) 437 | )) 438 | 439 | 440 | 441 | (deftest list-item-line 442 | (let [parse #(parser/parse % :start :list-item-line)] 443 | 444 | (testing "list-item-line with asterisk" 445 | (is (= [:list-item-line [:indent ""] [:list-item-bullet "*"] [:text [:text-normal "a simple list item"]]] 446 | (parse "* a simple list item")))) 447 | (testing "list-item-line with hyphen" 448 | (is (= [:list-item-line [:indent ""] [:list-item-bullet "-"] [:text [:text-normal "a simple list item"]]] 449 | (parse "- a simple list item")))) 450 | (testing "list-item-line with plus sign" 451 | (is (= [:list-item-line [:indent ""] [:list-item-bullet "+"] [:text [:text-normal "a simple list item"]]] 452 | (parse "+ a simple list item")))) 453 | (testing "list-item-line with counter and dot" 454 | (is (= [:list-item-line 455 | [:indent ""] 456 | [:list-item-counter "1"] 457 | [:list-item-counter-suffix "."] 458 | [:text [:text-normal "a simple list item"]]] 459 | (parse "1. a simple list item")))) 460 | (testing "list-item-line with counter and parentheses" 461 | (is (= [:list-item-line 462 | [:indent ""] 463 | [:list-item-counter "1"] 464 | [:list-item-counter-suffix ")"] 465 | [:text [:text-normal "a simple list item"]]] 466 | (parse "1) a simple list item")))) 467 | (testing "list-item-line with alphabetical counter and parentheses" 468 | (is (= [:list-item-line 469 | [:indent ""] 470 | [:list-item-counter "a"] 471 | [:list-item-counter-suffix ")"] 472 | [:text [:text-normal "a simple list item"]]] 473 | (parse "a) a simple list item")))) 474 | (testing "list-item-line with alphabetical counter and parentheses" 475 | (is (= [:list-item-line 476 | [:indent ""] 477 | [:list-item-counter "A"] 478 | [:list-item-counter-suffix ")"] 479 | [:text [:text-normal "a simple list item"]]] 480 | (parse "A) a simple list item")))) 481 | (testing "list-item-line with checkbox" 482 | (is (= [:list-item-line 483 | [:indent ""] 484 | [:list-item-bullet "-"] 485 | [:list-item-checkbox [:list-item-checkbox-state "X"]] 486 | [:text [:text-normal "a simple list item"]]] 487 | (parse "- [X] a simple list item")))) 488 | (testing "list-item-line with tag" 489 | (is (= [:list-item-line 490 | [:indent " "] 491 | [:list-item-bullet "*"] 492 | [:list-item-tag "a tag"] 493 | [:text [:text-normal "a simple list item"]]] 494 | (parse " * a tag :: a simple list item")))) 495 | (testing "list-item-line with checkbox and tag" 496 | (is (= [:list-item-line 497 | [:indent ""] 498 | [:list-item-bullet "-"] 499 | [:list-item-checkbox [:list-item-checkbox-state "X"]] 500 | [:list-item-tag "a tag"] 501 | [:text [:text-normal "a simple list item"]]] 502 | (parse "- [X] a tag :: a simple list item")))) 503 | )) 504 | 505 | (deftest keyword 506 | (let [parse #(parser/parse % :start :other-keyword-line)] 507 | (testing "keyword" 508 | (is (= [:other-keyword-line [:kw-name "HELLO"] [:kw-value "hello world"]] 509 | (parse "#+HELLO: hello world")))))) 510 | 511 | 512 | (deftest node-property 513 | (let [parse #(parser/parse % :start :node-property-line)] 514 | (testing "node-property" 515 | (is (= [:node-property-line [:node-property-name "HELLO"]] 516 | (parse ":HELLO:")))) 517 | (testing "node-property" 518 | (is (= [:node-property-line [:node-property-name "HELLO"] [:node-property-plus]] 519 | (parse ":HELLO+:")))) 520 | (testing "node-property" 521 | (is (= [:node-property-line 522 | [:node-property-name "HELLO"] 523 | [:node-property-value [:text [:text-normal "hello world"]]]] 524 | (parse ":HELLO: hello world")))) 525 | (testing "node-property" 526 | (is (= [:node-property-line 527 | [:node-property-name "HELLO"] 528 | [:node-property-plus] 529 | [:node-property-value [:text [:text-normal "hello world"]]]] 530 | (parse ":HELLO+: hello world")))) 531 | )) 532 | 533 | (deftest timestamp 534 | (let [parse #(parser/parse % :start :timestamp)] 535 | (testing "diary timestamp" 536 | (is (= [:timestamp [:timestamp-diary "(( <(sexp)().))"]] 537 | (parse "<%%(( <(sexp)().))>")))) 538 | (testing "date timestamp without day" 539 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-18"]] [:ts-modifiers]]]] 540 | (parse "<2020-01-18>")))) 541 | (testing "date timestamp with day" 542 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-18"] [:ts-day "Sat"]] [:ts-modifiers]]]] 543 | (parse "<2020-01-18 Sat>")))) 544 | (testing "date timestamp with day in other language" 545 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-21"] [:ts-day "Di"]] [:ts-modifiers]]]] 546 | (parse "<2020-01-21 Di>")))) 547 | (testing "date timestamp with day containing umlauts" 548 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-21"] [:ts-day "Dönerstag"]] [:ts-modifiers]]]] 549 | (parse "<2020-01-21 Dönerstag>")))) 550 | (testing "date timestamp without day and time" 551 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-w-time [:ts-date "2020-01-18"] [:ts-time "12:00"]] [:ts-modifiers]]]] 552 | (parse "<2020-01-18 12:00>")))) 553 | (testing "date timestamp with day and time" 554 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-w-time [:ts-date "2020-01-18"] [:ts-day "Sat"] [:ts-time "12:00"]] [:ts-modifiers]]]] 555 | (parse "<2020-01-18 Sat 12:00>")))) 556 | (testing "date timestamp with day and time with seconds" 557 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-w-time [:ts-date "2020-01-18"] [:ts-day "Sat"] [:ts-time "12:00:00"]] [:ts-modifiers]]]] 558 | (parse "<2020-01-18 Sat 12:00:00>")))) 559 | 560 | (testing "timestamp with repeater" 561 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-18"]] 562 | [:ts-modifiers [:ts-repeater [:ts-repeater-type "+"] 563 | [:ts-mod-value "1"] [:ts-mod-unit "w"]]]]]] 564 | (parse "<2020-01-18 +1w>")))) 565 | (testing "timestamp with warning" 566 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-18"]] 567 | [:ts-modifiers [:ts-warning [:ts-warning-type "-"] 568 | [:ts-mod-value "2"] [:ts-mod-unit "d"]]]]]] 569 | (parse "<2020-01-18 -2d>")))) 570 | (testing "timestamp with both repeater and warning" 571 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-18"]] 572 | [:ts-modifiers [:ts-repeater [:ts-repeater-type "+"] 573 | [:ts-mod-value "1"] [:ts-mod-unit "w"]] 574 | [:ts-warning [:ts-warning-type "-"] 575 | [:ts-mod-value "2"] [:ts-mod-unit "d"]]]]]] 576 | (parse "<2020-01-18 +1w -2d>")))) 577 | (testing "timestamp with both warning and repeater" 578 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2020-01-18"]] 579 | [:ts-modifiers [:ts-warning [:ts-warning-type "-"] [:ts-mod-value "2"] [:ts-mod-unit "d"]] 580 | [:ts-repeater [:ts-repeater-type "+"] [:ts-mod-value "1"] [:ts-mod-unit "w"]]]]]] 581 | (parse "<2020-01-18 -2d +1w>")))) 582 | (testing "timestamp with time and both warning and repeater" 583 | (is (= [:timestamp [:timestamp-active [:ts-inner 584 | [:ts-inner-w-time [:ts-date "2020-01-18"] [:ts-time "18:00"]] 585 | [:ts-modifiers 586 | [:ts-warning [:ts-warning-type "-"] [:ts-mod-value "2"] [:ts-mod-unit "d"]] 587 | [:ts-repeater [:ts-repeater-type "+"] [:ts-mod-value "1"] [:ts-mod-unit "w"]]]]]] 588 | (parse "<2020-01-18 18:00 -2d +1w>")))) 589 | 590 | (testing "timestamp with time span and both warning and repeater" 591 | (is (= [:timestamp [:timestamp-active [:ts-inner-span 592 | [:ts-inner-w-time [:ts-date "2020-01-18"] [:ts-time "18:00"]] 593 | [:ts-time "20:00"] 594 | [:ts-modifiers 595 | [:ts-warning [:ts-warning-type "-"] [:ts-mod-value "2"] [:ts-mod-unit "d"]] 596 | [:ts-repeater [:ts-repeater-type "+"] [:ts-mod-value "1"] [:ts-mod-unit "w"]]]]]] 597 | (parse "<2020-01-18 18:00-20:00 -2d +1w>")))) 598 | 599 | (testing "more than one space between parts of timestamp does not matter" 600 | (is (= [:timestamp [:timestamp-active [:ts-inner 601 | [:ts-inner-w-time [:ts-date "2020-01-18"] [:ts-time "18:00"]] 602 | [:ts-modifiers 603 | [:ts-warning [:ts-warning-type "-"] [:ts-mod-value "2"] [:ts-mod-unit "d"]] 604 | [:ts-repeater [:ts-repeater-type "+"] [:ts-mod-value "1"] [:ts-mod-unit "w"]]]]]] 605 | (parse "<2020-01-18 18:00 -2d +1w>")))) 606 | 607 | (testing "timestamp ranges" 608 | (is (= [:timestamp [:timestamp-active 609 | [:ts-inner [:ts-inner-wo-time [:ts-date "2020-04-25"]] [:ts-modifiers]] 610 | [:ts-inner [:ts-inner-wo-time [:ts-date "2020-04-28"]] [:ts-modifiers]]]] 611 | (parse "<2020-04-25>--<2020-04-28>")))) 612 | (testing "timestamp ranges with times" 613 | (is (= [:timestamp [:timestamp-active 614 | [:ts-inner [:ts-inner-w-time [:ts-date "2020-04-25"] [:ts-time "08:00"]] [:ts-modifiers]] 615 | [:ts-inner [:ts-inner-w-time [:ts-date "2020-04-28"] [:ts-time "16:00"]] [:ts-modifiers]]]] 616 | (parse "<2020-04-25 08:00>--<2020-04-28 16:00>")))) 617 | 618 | (testing "inactive timestamps" 619 | (is (= [:timestamp [:timestamp-inactive [:ts-inner-span 620 | [:ts-inner-w-time [:ts-date "2020-01-18"] [:ts-time "18:00"]] 621 | [:ts-time "20:00"] 622 | [:ts-modifiers 623 | [:ts-warning [:ts-warning-type "-"] [:ts-mod-value "2"] [:ts-mod-unit "d"]] 624 | [:ts-repeater [:ts-repeater-type "+"] [:ts-mod-value "1"] [:ts-mod-unit "w"]]]]]] 625 | (parse "[2020-01-18 18:00-20:00 -2d +1w]")))) 626 | 627 | (testing "syntactically wrong timestamp" 628 | (is (insta/failure? (parse "<2020-04-25 day wrong>")))) 629 | 630 | (testing "at-least modifier for habits" 631 | (is (= [:timestamp [:timestamp-active [:ts-inner 632 | [:ts-inner-wo-time [:ts-date "2009-10-17"] [:ts-day "Sat"]] 633 | [:ts-modifiers [:ts-repeater 634 | [:ts-repeater-type ".+"] [:ts-mod-value "2"] [:ts-mod-unit "d"] 635 | [:ts-mod-at-least [:ts-mod-value "4"] [:ts-mod-unit "d"]]]]]]] 636 | (parse "<2009-10-17 Sat .+2d/4d>")))) 637 | 638 | (testing "accept seconds in time" 639 | (is (= [:timestamp [:timestamp-active [:ts-inner [:ts-inner-w-time 640 | [:ts-date "2009-10-17"] [:ts-day "Sat"] [:ts-time "15:30:55"]] [:ts-modifiers]]]] 641 | (parse "<2009-10-17 Sat 15:30:55>")))) 642 | 643 | (testing "missing leading zeros in time are no problem" 644 | (is (= [:timestamp [:timestamp-active [:ts-inner 645 | [:ts-inner-w-time [:ts-date "2009-10-17"] [:ts-day "Sat"] [:ts-time "8:00"]] [:ts-modifiers]]]] 646 | (parse "<2009-10-17 Sat 8:00>")))) 647 | 648 | (testing "newlines are not recognized as space \\s" 649 | ;; http://xahlee.info/clojure/clojure_instaparse.html 650 | (is (insta/failure? (parse "<2020-04-17 F\nri>")))) 651 | (testing "newlines are not recognized as space" 652 | ;; http://xahlee.info/clojure/clojure_instaparse.html 653 | (is (insta/failure? (parse "<2020-04-17\nFri>")))))) 654 | 655 | 656 | (deftest timestamp-ts-time 657 | (let [parse #(parser/parse % :start :ts-time)] 658 | (testing "parse time" 659 | (is (= [:ts-time "08:00"] 660 | (parse "08:00")))) 661 | (testing "parse time without leading zero" 662 | (is (= [:ts-time "8:00"] 663 | (parse "8:00")))) 664 | (testing "parse time with seconds" 665 | (is (= [:ts-time "08:00:00"] 666 | (parse "08:00:00")))) 667 | (testing "parse time a.m." 668 | (is (= [:ts-time "8:00AM"] 669 | (parse "8:00AM")))) 670 | (testing "parse time p.m." 671 | (is (= [:ts-time "08:00pm"] 672 | (parse "08:00pm")))))) 673 | 674 | (deftest timestamp-inactive-ranges 675 | (let [parse #(parser/parse % :start :timestamp-inactive-range)] 676 | (testing "parse inactive range" 677 | (is (= [:timestamp-inactive-range [:ts-inner-span 678 | [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:26"]] 679 | [:ts-time "23:46"] 680 | [:ts-modifiers]]] 681 | (parse "[2021-05-22 Sat 23:26-23:46]")))) 682 | (testing "parse inactive long range" 683 | (is (= [:timestamp-inactive-range 684 | [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:26"]] 685 | [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:46"]]] 686 | (parse "[2021-05-22 Sat 23:26]--[2021-05-22 Sat 23:46]")))) 687 | (testing "do not parse active range" 688 | (is (insta/failure? (parse "<2021-05-22 Sat 23:26-23:46>")))) 689 | (testing "do not parse inactive timestamp without range" 690 | (is (insta/failure? (parse "[2021-05-22 Sat 23:26]")))) 691 | )) 692 | 693 | (deftest literal-line 694 | (let [parse #(parser/parse % :start :fixed-width-line)] 695 | (testing "parse empty fixed-width line starting with colon (discards single trailing space)" 696 | (is (= [:fixed-width-line ""] 697 | (parse ": ")))) 698 | (testing "parse empty fixed-width line starting with colon" 699 | (is (= [:fixed-width-line ""] 700 | (parse ":")))) 701 | (testing "parse fixed-width line starting with colon" 702 | (is (= [:fixed-width-line " literal text"] 703 | (parse ": literal text")))) 704 | (testing "parse fixed-width line starting with spaces" 705 | (is (= [:fixed-width-line "literal text "] 706 | (parse " : literal text ")))) 707 | (testing "fail to parse fixed-width line with no space after colon" 708 | (is (insta/failure? (parse ":literal text")))) 709 | )) 710 | 711 | (deftest fixed-width-area 712 | (let [parse #(parser/parse % :start :fixed-width-area)] 713 | (testing "parse fixed-width area starting with colon" 714 | (is (= [:fixed-width-area 715 | [:fixed-width-line "foo "] 716 | [:fixed-width-line "bar"]] 717 | (parse " : foo \n : bar")))) 718 | )) 719 | 720 | (deftest links 721 | (let [parse #(parser/parse % :start :link-format)] 722 | (testing "parse simple link" 723 | (is (= [:link-format [:link [:link-ext [:link-ext-other 724 | [:link-url-scheme "https"] 725 | [:link-url-rest "//example.com"]]]]] 726 | (parse "[[https://example.com]]")))) 727 | (testing "parse simple link that looks like an web address but is not valid" 728 | (is (= [:link-format [:link [:link-int [:link-file-loc-string "www.example.com"]]]] 729 | (parse "[[www.example.com]]")))) 730 | (testing "parse link with description" 731 | (is (= [:link-format [:link [:link-ext [:link-ext-other 732 | [:link-url-scheme "https"] 733 | [:link-url-rest "//example.com"]]]] 734 | [:link-description "description words"]] 735 | (parse "[[https://example.com][description words]]")))) 736 | (testing "parse id link" 737 | (is (= [:link-format [:link [:link-ext [:link-ext-id "abc-123"]]]] 738 | (parse "[[id:abc-123]]")))) 739 | (testing "parse internal * link" 740 | (is (= [:link-format [:link [:link-int [:link-file-loc-customid "my-custom-id"]]]] 741 | (parse "[[#my-custom-id]]")))) 742 | (testing "parse internal # link" 743 | (is (= [:link-format [:link [:link-int [:link-file-loc-headline "My Header"]]]] 744 | (parse "[[*My Header]]")))) 745 | (testing "parse internal link" 746 | (is (= [:link-format [:link [:link-int [:link-file-loc-string "A Name"]]]] 747 | (parse "[[A Name]]")))) 748 | )) 749 | 750 | (deftest id-links 751 | (let [parse #(parser/parse % :start :link-ext-id)] 752 | (testing "invalid id link" 753 | (is (insta/failure? (parse "[[id:]]")))) 754 | (testing "invalid id link" 755 | (is (insta/failure? (parse "[[id:z]]")))) 756 | )) 757 | 758 | (deftest links-with-escapse 759 | (let [parse #(parser/parse % :start :link-format)] 760 | ;; remember that "\\" is one backslash! 761 | (testing "parse link with just one literal backslash" 762 | (is (insta/failure? (parse "[[\\]]")))) 763 | (testing "parse link with escaped backslash" 764 | (is (= [:link-format [:link [:link-int [:link-file-loc-string "\\\\"]]]] 765 | (parse "[[\\\\]]")))) 766 | (testing "parse link with unescaped backslash" 767 | (is (= [:link-format [:link [:link-int [:link-file-loc-string "\\a"]]]] 768 | (parse "[[\\a]]")))) 769 | (testing "parse link with unescaped opening bracket" 770 | (is (insta/failure? (parse "[[a[b]]")))) 771 | (testing "parse link with escaped opening bracket" 772 | (is (= [:link-format [:link [:link-int [:link-file-loc-string "\\["]]]] 773 | (parse "[[\\[]]")))) 774 | (testing "parse link with escaped closing bracket" 775 | (is (= [:link-format [:link [:link-int [:link-file-loc-string "\\]"]]]] 776 | (parse "[[\\]]]")))) 777 | )) 778 | 779 | (deftest links-external-file 780 | (let [parse #(parser/parse % :start :link-ext-file)] 781 | (testing "parse file link" 782 | (is (= [:link-ext-file "folder/file.txt"] 783 | (parse "file:folder/file.txt")))) 784 | (testing "parse file link" 785 | (is (= [:link-ext-file "~/folder/file.txt"] 786 | (parse "file:~/folder/file.txt")))) 787 | (testing "parse file link containing single colons" 788 | (is (= [:link-ext-file "~/fol:der/fi:le.txt"] 789 | (parse "file:~/fol:der/fi:le.txt")))) 790 | (testing "parse relative file link" 791 | (is (= [:link-ext-file "./folder/file.txt"] 792 | (parse "./folder/file.txt")))) 793 | (testing "parse absolute file link" 794 | (is (= [:link-ext-file "/folder/file.txt"] 795 | (parse "/folder/file.txt")))) 796 | (testing "parse file link with line number" 797 | (is (= [:link-ext-file "./file.org" [:link-file-loc-lnum "15"]] 798 | (parse "./file.org::15")))) 799 | (testing "parse file link with text search string" 800 | (is (= [:link-ext-file "./file.org" [:link-file-loc-string "foo bar"]] 801 | (parse "./file.org::foo bar")))) 802 | (testing "parse file link with text search string containing ::" 803 | ;; this matches orgmode behavior 804 | (is (= [:link-ext-file "./file.org" [:link-file-loc-string "foo::bar"]] 805 | (parse "./file.org::foo::bar")))) 806 | (testing "parse file link with headline" 807 | (is (= [:link-ext-file "./file.org" [:link-file-loc-headline "header1: test"]] 808 | (parse "./file.org::*header1: test")))) 809 | (testing "parse file link with custom id" 810 | (is (= [:link-ext-file "./file.org" [:link-file-loc-customid "custom-id"]] 811 | (parse "./file.org::#custom-id")))))) 812 | 813 | (deftest links-external-other-url 814 | (let [parse #(parser/parse % :start :link-ext-other)] 815 | (testing "parse simple link that looks like an web address but is not valid" 816 | (is (insta/failure? (parse "www.example.com")))) 817 | (testing "parse other http link" 818 | (is (= [:link-ext-other [:link-url-scheme "https"] [:link-url-rest "//example.com"]] 819 | (parse "https://example.com")))) 820 | (testing "parse other mailto link" 821 | (is (= [:link-ext-other [:link-url-scheme "mailto"] [:link-url-rest "info@example.com"]] 822 | (parse "mailto:info@example.com")))) 823 | (testing "parse other link with uncommon scheme" 824 | (is (= [:link-ext-other [:link-url-scheme "zyx"] [:link-url-rest "rest-of uri ..."]] 825 | (parse "zyx:rest-of uri ...")))))) 826 | 827 | (deftest embedded-in-text 828 | (let [parse #(parser/parse % :start :text)] 829 | (testing "parse timestamp after text" 830 | (is (= [:text 831 | [:text-normal "text before "] 832 | [:timestamp 833 | [:timestamp-active 834 | [:ts-inner 835 | [:ts-inner-w-time 836 | [:ts-date "2021-05-22"] 837 | [:ts-day "Sat"] 838 | [:ts-time "00:12"]] 839 | [:ts-modifiers]]]] 840 | [:text-normal " after"]] 841 | (parse "text before <2021-05-22 Sat 00:12> after")))) 842 | (testing "parse link after text" 843 | (is (= [:text 844 | [:text-normal "text before "] 845 | [:link-format 846 | [:link 847 | [:link-ext 848 | [:link-ext-other 849 | [:link-url-scheme "http"] 850 | [:link-url-rest "//example.com"]]]]] 851 | [:text-normal " after"]] 852 | (parse "text before [[http://example.com]] after")))) 853 | )) 854 | 855 | 856 | (deftest text-styled 857 | (let [parse #(parser/parse % :start :text-styled)] 858 | (testing "parse bold text" 859 | (is (= [[:text-sty-bold "bold text"]] 860 | (parse "*bold text*")))) 861 | (testing "parse italic text" 862 | (is (= [[:text-sty-italic "italic text"]] 863 | (parse "/italic text/")))) 864 | (testing "parse underlined text" 865 | (is (= [[:text-sty-underlined "underlined text"]] 866 | (parse "_underlined text_")))) 867 | (testing "parse verbatim text" 868 | (is (= [[:text-sty-verbatim "verbatim /abc/ text"]] 869 | (parse "=verbatim /abc/ text=")))) 870 | (testing "parse code text" 871 | (is (= [[:text-sty-code "code *abc* text"]] 872 | (parse "~code *abc* text~")))) 873 | (testing "parse strike-through text" 874 | (is (= [[:text-sty-strikethrough "strike-through text"]] 875 | (parse "+strike-through text+")))) 876 | ;; parse reluctant 877 | ;; (testing "parse text-styled alone is not reluctant" 878 | ;; (is (not (insta/failure? (parse "/italic/ italic/"))))) 879 | (testing "parse verbatim text reluctantly" 880 | (is (insta/failure? (parse "=verbatim= text=")))) 881 | 882 | ;; parse special cases 883 | (testing "not parse empty verbatim text" 884 | (is (insta/failure? (parse "==")))) 885 | (testing "not parse verbatim text with space around" 886 | (is (insta/failure? (parse "=verbatim =")))) 887 | (testing "not parse verbatim text with space around" 888 | (is (insta/failure? (parse "= verbatim=")))) 889 | (testing "parse verbatim text" 890 | (is (= [[:text-sty-verbatim "verbatim = text"]] 891 | (parse "=verbatim = text=")))) 892 | (testing "parse verbatim text" 893 | (is (= [[:text-sty-verbatim "="]] 894 | (parse "===")))) 895 | (testing "parse verbatim text" 896 | (is (= [[:text-sty-verbatim "a"]] 897 | (parse "=a=")))) 898 | )) 899 | 900 | (deftest text-link 901 | (let [parse #(parser/parse % :start :text-link)] 902 | (testing "parse angled link" 903 | (is (= [:text-link [:text-link-angle 904 | [:link-url-scheme "http"] 905 | [:text-link-angle-path "//example.com/foo?bar=baz&baz=bar"]]] 906 | (parse "<http://example.com/foo?bar=baz&baz=bar>")))) 907 | (testing "parse plain link" 908 | (is (= [:text-link [:text-link-plain 909 | [:link-url-scheme "http"] 910 | [:text-link-plain-path "//example.com/foo?bar=baz&baz=bar"]]] 911 | (parse "http://example.com/foo?bar=baz&baz=bar")))) 912 | )) 913 | 914 | (deftest text 915 | (let [parse #(parser/parse % :start :text)] 916 | (testing "stop parsing text at EOL" 917 | (is (= [:text [:text-normal "abc "]] 918 | (parse "abc ")))) 919 | (testing "does not parse a string starting with newline" 920 | (is (insta/failure? (parse "\nfoo")))) 921 | (testing "parse text that contains style delimiter" 922 | (is (= [:text [:text-normal "a"] [:text-normal "/b"]] 923 | (parse "a/b")))) 924 | (testing "parse text that contains style delimiter" 925 | (is (= [:text [:text-normal "a "] [:text-normal "/b"]] 926 | (parse "a /b")))) 927 | (testing "parse styled text alone" 928 | (is (= [:text [:text-sty-bold "bold text"]] 929 | (parse "*bold text*")))) 930 | (testing "parse styled text followed by normal text" 931 | (is (= [:text [:text-sty-bold "bold text"] 932 | [:text-normal " normal text"]] 933 | (parse "*bold text* normal text")))) 934 | (testing "parse normal text followed by styled text" 935 | (is (= [:text [:text-normal "normal text "] 936 | [:text-sty-bold "bold text"]] 937 | (parse "normal text *bold text*")))) 938 | (testing "parse styled text surrounded by normal text" 939 | (is (= [:text 940 | [:text-normal "normal text "] 941 | [:text-sty-bold "bold text"] 942 | [:text-normal " more text"]] 943 | (parse "normal text *bold text* more text")))) 944 | (testing "parse styled text reluctant" 945 | (is (= [:text [:text-sty-bold "bold text"] 946 | [:text-normal " text"] 947 | [:text-normal "*"]] 948 | (parse "*bold text* text*")))) 949 | ;; TODO parse only when "surrounded" by delimiter 950 | ;; (testing "parse italic text" 951 | ;; (is (= [:text [:text-styled [:text-sty-italic 952 | ;; [:text [:text-normal "italic "] [:text-normal "/ text"]]]]] 953 | ;; (parse "/italic / text/")))) 954 | (testing "parse angled text link surrounded by normal text" 955 | (is (= [:text 956 | [:text-normal "normal text "] 957 | [:text-link [:text-link-angle 958 | [:link-url-scheme "http"] 959 | [:text-link-angle-path "//example.com"]]] 960 | [:text-normal " more text"]] 961 | (parse "normal text <http://example.com> more text")))) 962 | ;; TODO (testing "parse normal text link surrounded by normal text" 963 | ;; (is (= [:text 964 | ;; [:text-normal "normal text "] 965 | ;; [:text-link [:text-link-plain "http://example.com"]] 966 | ;; [:text-normal " more text"]] 967 | ;; (parse "normal text http://example.com more text")))) 968 | (testing "parse link surrounded by normal text" 969 | (is (= [:text 970 | [:text-normal "normal text "] 971 | [:link-format [:link [:link-ext [:link-ext-other 972 | [:link-url-scheme "http"] [:link-url-rest "//example.com"]]]]] 973 | [:text-normal " more text"]] 974 | (parse "normal text [[http://example.com]] more text")))) 975 | (testing "parse link followed by footnote" 976 | (is (= [:text 977 | [:text-normal "normal text "] 978 | [:link-format [:link [:link-ext [:link-ext-other 979 | [:link-url-scheme "http"] 980 | [:link-url-rest "//example.com"]]]]] 981 | [:footnote-link "reserved"]] 982 | (parse "normal text [[http://example.com]][fn::reserved]")))) 983 | ;; TODO this is not a subscript (space before) 984 | ;; (testing "parse non-subscript" 985 | ;; (is (= [:text [:text-normal "text _abc"]] 986 | ;; (parse "text _abc")))) 987 | (testing "parse subscript" 988 | (is (= [:text [:text-normal "text"] [:text-sub [:text-subsup-word "abc"]]] 989 | (parse "text_abc")))) 990 | (testing "parse superscript" 991 | (is (= [:text [:text-normal "text"] [:text-sup [:text-subsup-word "123"]]] 992 | (parse "text^123")))) 993 | (testing "parse sub- and superscript" 994 | (is (= [:text [:text-normal "text"] 995 | [:text-sup [:text-subsup-word "abc"]] 996 | [:text-sub [:text-subsup-curly "123"]]] 997 | (parse "text^abc_{123}")))) 998 | 999 | ;; line breaks 1000 | (testing "parse text followed by line break" 1001 | (is (= [:text [:text-normal "abc "] [:text-linebreak [:text-linebreak-after " "]]] 1002 | (parse "abc \\\\ ")))) 1003 | (testing "parse text followed by line break" 1004 | (is (= [:text [:text-normal "abc "] [:text-normal "\\"] [:text-normal "\\ xyz"]] 1005 | (parse "abc \\\\ xyz")))) 1006 | 1007 | ;; macros 1008 | (testing "parse macro" 1009 | (is (= [:text [:text-normal "text"] [:text-macro 1010 | [:macro-name "my_macro5"] 1011 | [:macro-args "0" "'{abc}'"]]] 1012 | (parse "text{{{my_macro5(0,'{abc}')}}}")))) 1013 | 1014 | ;; targets and radio targets 1015 | (testing "parse target" 1016 | (is (= [:text [:text-normal "text"] [:text-target [:text-target-name "my target"]]] 1017 | (parse "text<<my target>>")))) 1018 | (testing "parse radio target" 1019 | (is (= [:text [:text-normal "text"] [:text-radio-target [:text-target-name "my target"]]] 1020 | (parse "text<<<my target>>>")))) 1021 | 1022 | ;; entities 1023 | (testing "parse entity" 1024 | (is (= [:text [:text-normal "text"] 1025 | [:text-entity [:entity-name "Alpha"]] 1026 | [:text-normal "-followed"]] 1027 | (parse "text\\Alpha-followed")))) 1028 | (testing "parse entity" 1029 | (is (= [:text [:text-normal "text "] 1030 | [:text-entity [:entity-name "Alpha"] [:entity-braces]] 1031 | [:text-normal "followed"]] 1032 | (parse "text \\Alpha{}followed")))) 1033 | 1034 | )) 1035 | 1036 | 1037 | (deftest text-macros 1038 | (let [parse #(parser/parse % :start :text-macro)] 1039 | (testing "parse macro without args" 1040 | (is (= [:text-macro [:macro-name "my_macro5"] [:macro-args ""]] 1041 | (parse "{{{my_macro5()}}}")))) 1042 | (testing "parse macro with arg" 1043 | (is (= [:text-macro [:macro-name "my_macro5"] [:macro-args "arg"]] 1044 | (parse "{{{my_macro5(arg)}}}")))) 1045 | (testing "parse macro" 1046 | (is (= [:text-macro [:macro-name "my_macro5"] [:macro-args "x\\,y" " (0)", "'{abc}'"]] 1047 | (parse "{{{my_macro5(x\\,y, (0),'{abc}')}}}")))) 1048 | )) 1049 | 1050 | (deftest text-entities 1051 | (let [parse #(parser/parse % :start :text-entity)] 1052 | (testing "parse entity" 1053 | (is (= [:text-entity [:entity-name "Alpha"]] 1054 | (parse "\\Alpha")))) 1055 | (testing "parse entity" 1056 | (is (= [:text-entity [:entity-name "Alpha"] [:entity-braces]] 1057 | (parse "\\Alpha{}")))) 1058 | )) 1059 | 1060 | (deftest text-targets 1061 | (let [parse #(parser/parse % :start :text-target)] 1062 | (testing "parse target" 1063 | (is (= [:text-target [:text-target-name "t"]] 1064 | (parse "<<t>>")))) 1065 | (testing "parse invalid target" 1066 | (is (insta/failure? 1067 | (parse "<< t>>")))) 1068 | (testing "parse invalid target" 1069 | (is (insta/failure? 1070 | (parse "<<t >>")))) 1071 | (testing "parse invalid target" 1072 | (is (insta/failure? 1073 | (parse "<< >>")))) 1074 | )) 1075 | 1076 | (deftest text-subscript 1077 | (let [parse #(parser/parse % :start :text-sub)] 1078 | ;; TODO make sure preceeding character is non-whitespace 1079 | (testing "parse subscript word" 1080 | (is (= [:text-sub [:text-subsup-word "abc"]] 1081 | (parse "_abc")))) 1082 | (testing "parse subscript number" 1083 | (is (= [:text-sub [:text-subsup-word "123"]] 1084 | (parse "_123")))) 1085 | (testing "parse subscript word/number mixed" 1086 | (is (= [:text-sub [:text-subsup-word "1a2b"]] 1087 | (parse "_1a2b")))) 1088 | (testing "parse subscript star" 1089 | (is (= [:text-sub [:text-subsup-word "*"]] 1090 | (parse "_*")))) 1091 | (testing "parse subscript special" 1092 | (is (= [:text-sub [:text-subsup-word ".,\\a"]] 1093 | (parse "_.,\\a")))) 1094 | (testing "parse subscript special" 1095 | (is (= [:text-sub [:text-subsup-word "-.,\\a"]] 1096 | (parse "_-.,\\a")))) 1097 | (testing "parse subscript special" 1098 | (is (= [:text-sub [:text-subsup-word "+.,\\a"]] 1099 | (parse "_+.,\\a")))) 1100 | (testing "parse subscript in curly braces" 1101 | (is (= [:text-sub [:text-subsup-curly ".,-123abc!"]] 1102 | (parse "_{.,-123abc!}")))) 1103 | (testing "curly braces inside braced subscript are not allowed" 1104 | (is (insta/failure? (parse "text_{{}")))) 1105 | )) 1106 | 1107 | (deftest tables 1108 | (let [parse #(parser/parse % :start :table)] 1109 | (testing "parse table.el table" 1110 | (is (= [:table [:table-tableel 1111 | [:table-tableel-sep "+---+"] 1112 | [:table-tableel-line "| x |"] 1113 | [:table-tableel-sep "+---+"]]] 1114 | (parse "+---+\n| x |\n+---+\n")))) 1115 | (testing "parse table.el table with preceding whitespace" 1116 | (is (= [:table [:table-tableel 1117 | [:table-tableel-sep "+---+"] 1118 | [:table-tableel-line "| x |"] 1119 | [:table-tableel-sep "+---+"]]] 1120 | (parse " +---+\n | x |\n +---+\n")))) 1121 | (testing "parse org table" 1122 | (is (= [:table 1123 | [:table-org 1124 | [:table-row [:table-row-sep "|--+--|"]] 1125 | [:table-row [:table-row-cells [:table-cell " x"] [:table-cell "x "]]] 1126 | [:table-row [:table-row-sep "|--+--|"]]]] 1127 | (parse " |--+--|\n | x|x |\n |--+--|\n")))) 1128 | (testing "parse org table with formulas" 1129 | (is (= [:table 1130 | [:table-org 1131 | [:table-row [:table-row-sep "|--+--|"]] 1132 | [:table-row [:table-row-cells [:table-cell " x"] [:table-cell "x "]]] 1133 | [:table-row [:table-row-sep "|--+--|"]] 1134 | [:table-formula "$4=vmean($2..$3)"]]] 1135 | (parse " |--+--|\n | x|x |\n |--+--|\n #+TBLFM: $4=vmean($2..$3)")))) 1136 | )) 1137 | 1138 | 1139 | (deftest clock 1140 | (let [parse #(parser/parse % :start :clock)] 1141 | (testing "a simple clock line" 1142 | (is (= [:clock [:timestamp-inactive-range 1143 | [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:26"]] 1144 | [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:46"]]] 1145 | [:clock-duration [:clock-dur-hh "0"] [:clock-dur-mm "20"]]] 1146 | (parse "CLOCK: [2021-05-22 Sat 23:26]--[2021-05-22 Sat 23:46] => 0:20")))) 1147 | (testing "a simple clock line" 1148 | (is (= [:clock [:timestamp-inactive-range [:ts-inner-span 1149 | [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:26"]] 1150 | [:ts-time "23:46"] 1151 | [:ts-modifiers]]] 1152 | [:clock-duration [:clock-dur-hh "0"] [:clock-dur-mm "20"]]] 1153 | (parse " CLOCK: [2021-05-22 Sat 23:26-23:46] => 0:20 ")))) 1154 | (testing "do not parse corrupted headlines" 1155 | (is (insta/failure? (parse "CLOCK: [not a timestamp Sat 23:26] => 0:20 ")))) 1156 | )) 1157 | 1158 | 1159 | (deftest diary-sexp 1160 | (let [parse #(parser/parse % :start :diary-sexp)] 1161 | (testing "a simple diary sexp line" 1162 | (is (= [:diary-sexp "nr()<n)-h"] 1163 | (parse "%%(nr()<n)-h")))) 1164 | (testing "do not parse if not starting at begin of line" 1165 | (is (insta/failure? (parse " %%(x)")))) 1166 | )) 1167 | 1168 | 1169 | (deftest planning 1170 | (let [parse #(parser/parse % :start :planning)] 1171 | (testing "a simple planning info" 1172 | (is (= [:planning [:planning-info 1173 | [:planning-keyword [:planning-kw-scheduled]] 1174 | [:timestamp [:timestamp-inactive [:ts-inner [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:26"]] [:ts-modifiers]]]]]] 1175 | (parse "SCHEDULED: [2021-05-22 Sat 23:26]")))) 1176 | (testing "a planning info with spaces" 1177 | (is (= [:planning [:planning-info 1178 | [:planning-keyword [:planning-kw-deadline]] 1179 | [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2021-05-22"] [:ts-day "Sat"]] [:ts-modifiers]]]]]] 1180 | (parse " DEADLINE: <2021-05-22 Sat> ")))) 1181 | (testing "all planning items in a row" 1182 | (is (= [:planning 1183 | [:planning-info 1184 | [:planning-keyword [:planning-kw-scheduled]] 1185 | [:timestamp [:timestamp-inactive [:ts-inner [:ts-inner-w-time [:ts-date "2021-05-22"] [:ts-day "Sat"] [:ts-time "23:26"]] [:ts-modifiers]]]]] 1186 | [:planning-info 1187 | [:planning-keyword [:planning-kw-deadline]] 1188 | [:timestamp [:timestamp-active [:ts-inner [:ts-inner-wo-time [:ts-date "2021-05-22"] [:ts-day "Sat"]] [:ts-modifiers]]]]] 1189 | [:planning-info 1190 | [:planning-keyword [:planning-kw-closed]] 1191 | [:timestamp [:timestamp-inactive [:ts-inner [:ts-inner-wo-time [:ts-date "2021-05-21"] [:ts-day "Fri"]] [:ts-modifiers]]]]]] 1192 | (parse "SCHEDULED: [2021-05-22 Sat 23:26] DEADLINE: <2021-05-22 Sat> CLOSED: [2021-05-21 Fri] ")))) 1193 | )) 1194 | 1195 | (deftest whole-files 1196 | (testing "headlines and tables" 1197 | (let [content (slurp "test/org_parser/fixtures/headlines_and_tables.org")] 1198 | (is (= [:S 1199 | [:headline [:stars "*"] [:text [:text-normal "Headline 1"]]] 1200 | [:empty-line] 1201 | [:table 1202 | [:table-org 1203 | [:table-row 1204 | [:table-row-cells 1205 | [:table-cell " first column 1 "] 1206 | [:table-cell " first column 2 "]]] 1207 | [:table-row 1208 | [:table-row-cells 1209 | [:table-cell " first value 1 "] 1210 | [:table-cell " first value 2 "]]]]] 1211 | [:table 1212 | [:table-org 1213 | [:table-row 1214 | [:table-row-cells 1215 | [:table-cell " second column 1 "] 1216 | [:table-cell " second column 2 "]]] 1217 | [:table-row 1218 | [:table-row-cells 1219 | [:table-cell " second value 1 "] 1220 | [:table-cell " second value 2 "]]]]] 1221 | [:headline [:stars "*"] [:text [:text-normal "Headline 2"]]] 1222 | [:empty-line] 1223 | [:table 1224 | [:table-org 1225 | [:table-row 1226 | [:table-row-cells 1227 | [:table-cell " people "] 1228 | [:table-cell " age "]]] 1229 | [:table-row [:table-row-sep "|------------+-----|"]] 1230 | [:table-row 1231 | [:table-row-cells 1232 | [:table-cell " bob "] 1233 | [:table-cell " 38 "]]] 1234 | [:table-row 1235 | [:table-row-cells 1236 | [:table-cell " max "] 1237 | [:table-cell " 42 "]]] 1238 | [:table-row [:table-row-sep "|------------+-----|"]] 1239 | [:table-row 1240 | [:table-row-cells 1241 | [:table-cell " median age "] 1242 | [:table-cell " 40 "]]] 1243 | [:table-formula "@4$2=vmean(@2..@-1)"]]] 1244 | [:headline [:stars "*"] [:text [:text-normal "table.el style table"]]] 1245 | [:empty-line] 1246 | [:content-line 1247 | [:text 1248 | [:text-normal 1249 | " The option to use org tables and table.el tables is documented in"]]] 1250 | [:content-line 1251 | [:text 1252 | ;; FIXME: URLs seem to get mangled. Ignoring it here, 1253 | ;; because this test is about org and table.el tables. 1254 | [:text-normal " the spec: https:"] 1255 | [:text-normal "/"] 1256 | [:text-normal "/orgmode.org"] 1257 | [:text-normal "/worg"] 1258 | [:text-normal "/dev"] 1259 | [:text-normal "/org-syntax.html#Tables"]]] 1260 | [:empty-line] 1261 | [:content-line 1262 | [:text 1263 | [:text-normal " Hence, "] 1264 | [:text-sty-verbatim "org-parser"] 1265 | [:text-normal " should and does parse it!"]]] 1266 | [:empty-line] 1267 | [:table 1268 | [:table-tableel 1269 | [:table-tableel-sep "+-----+-----+"] 1270 | [:table-tableel-line "| people | age |"] 1271 | [:table-tableel-sep "+-----+-----+"] 1272 | [:table-tableel-line "| bob | 38 |"] 1273 | [:table-tableel-sep "+-----+-----+"] 1274 | [:table-tableel-line "| max | 42 |"] 1275 | [:table-tableel-sep "+-----+-----+"]]]] 1276 | (parser/parse content)))))) 1277 | -------------------------------------------------------------------------------- /test/org_parser/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns org-parser.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | org-parser.core-test 4 | org-parser.parser-test 5 | org-parser.parser-mean-test 6 | org-parser.transform-test)) 7 | 8 | 9 | ;; This isn't strictly necessary, but is a good idea depending 10 | ;; upon your application's ultimate runtime engine. 11 | (enable-console-print!) 12 | 13 | 14 | (doo-tests 15 | 'org-parser.core-test 16 | 'org-parser.parser-test 17 | 'org-parser.parser-mean-test 18 | 'org-parser.transform-test) 19 | -------------------------------------------------------------------------------- /test/org_parser/transform_test.cljc: -------------------------------------------------------------------------------- 1 | (ns org-parser.transform-test 2 | (:require [org-parser.transform :as sut] 3 | #?(:clj [clojure.test :refer :all] 4 | :cljs [cljs.test :refer-macros [deftest is testing]]))) 5 | 6 | 7 | (def props 8 | [[:stars "*"] [:title "hello" "world"]]) 9 | 10 | 11 | (deftest property 12 | (testing "helper fn" 13 | (is (= ["hello" "world"] 14 | (#'sut/property :title props))))) 15 | 16 | (deftest merge-consecutive-text-normal 17 | (testing "works for simple case with spaces" 18 | (is (= [:text [:text-normal "a bc"]] 19 | (apply #'sut/merge-consecutive-text-normal [[:text-normal "a "] [:text-normal "b"] [:text-normal "c"]]))))) 20 | 21 | (deftest heading-with-tags 22 | (testing "heading with tags" 23 | (is (= [[:text-normal "title"] ["_" "tag1"]] 24 | (#'sut/extract-tags [:text-normal "title :_:tag1:"]))))) 25 | --------------------------------------------------------------------------------