├── .gitignore ├── Changes ├── devnotes.org ├── dist.ini ├── lib └── Org │ ├── Document.pm │ ├── Element.pm │ ├── Element │ ├── Block.pm │ ├── Comment.pm │ ├── Drawer.pm │ ├── FixedWidthSection.pm │ ├── Footnote.pm │ ├── Headline.pm │ ├── Link.pm │ ├── List.pm │ ├── ListItem.pm │ ├── RadioTarget.pm │ ├── Setting.pm │ ├── Table.pm │ ├── TableCell.pm │ ├── TableHLine.pm │ ├── TableRow.pm │ ├── Target.pm │ ├── Text.pm │ ├── TimeRange.pm │ └── Timestamp.pm │ ├── ElementRole.pm │ ├── ElementRole │ ├── Block.pm │ └── Inline.pm │ └── Parser.pm ├── t ├── 01-basics.t ├── base_element-field_name.t ├── base_element.t ├── block.t ├── comment.t ├── data │ ├── custom_todo_kw.org │ ├── listitem.org │ ├── todo-setting-after-todos.org │ ├── unicode │ │ ├── fr.org │ │ ├── hr.org │ │ ├── latin1.org │ │ ├── latin2.org │ │ └── zh.org │ └── various.org ├── document.t ├── drawer.t ├── fixed_width_section.t ├── footnote.t ├── headline-get_property.t ├── headline.t ├── link_and_target.t ├── list.t ├── radio_target.t ├── regression-rt68443.t ├── rt98375.t ├── setting-todo.t ├── setting.t ├── table.t ├── testlib.pl ├── text.t ├── timerange.t ├── timestamp.t ├── unicode.t └── various.t ├── todo.org └── weaver.ini /.gitignore: -------------------------------------------------------------------------------- 1 | Org-Parser-* 2 | .build 3 | *~ 4 | nytprof* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | 0.561 2023-11-06 Released-By: PERLANCAR; Urgency: low 2 | 3 | - No functional changes. 4 | 5 | - [doc] Typo in heading numbering (thanks William Lindley). 6 | 7 | 8 | 0.560 2023-08-05 Released-By: PERLANCAR; Urgency: medium 9 | 10 | - [ux] In error message, show filename to ease debugging. 11 | 12 | 13 | 0.559 2023-07-12 Released-By: PERLANCAR; Urgency: medium 14 | 15 | - No functional changes. 16 | 17 | - Remove usage of smartmatch to avoid deprecation warning under 18 | perl 5.38. 19 | 20 | 21 | 0.558 2022-06-23 Released-By: PERLANCAR; Urgency: medium 22 | 23 | - [bugfix] The parent of a more-indented list should be *the last 24 | listitem* of a lesser-indented list, not the lesser-indented list 25 | itself. 26 | 27 | - Rename Org::Element::Role -> Org::ElementRole, 28 | Org::Element::{Block,Inline}Role -> Org::ElementRole::{Block,Inline} 29 | so that Org::Element:: namespace is purely for element objects. 30 | 31 | 32 | 0.557 2022-03-27 Released-By: PERLANCAR; Urgency: medium 33 | 34 | - Bump minimum perl version to 5.014 due to compilation failures in 35 | older perls (RT#141560). 36 | 37 | 38 | 0.556 2022-02-08 Released-By: PERLANCAR; Urgency: medium 39 | 40 | - [class Org::Element] Add method settings(). 41 | 42 | 43 | 0.555 2021-06-27 Released-By: PERLANCAR; Urgency: medium; Backward-Incompatible: yes 44 | 45 | - [incompatible change] Rename Org::Element::Table{V,H}Line (thanks 46 | William Lindley). 47 | 48 | 49 | 0.554 2020-12-30 Released-By: PERLANCAR; Urgency: medium 50 | 51 | - [compliance] Accept non-latin numbers/letters as per orgmode doc: 52 | 'Tags are normal words containing letters, numbers, ‘_’, and 53 | ‘@’. (thanks Bernhard Graf!) 54 | 55 | - Parse German timestamps (thanks Tekki). 56 | 57 | 58 | 0.553 2020-09-17 Released-By: PERLANCAR; Urgency: medium 59 | 60 | - [INCOMPATIBLE CHANGE] Drawer: properties() now returns unparsed 61 | values, e.g. ':Title: foo bar baz' now because {Title => 'foo bar 62 | baz'} instead of {Title => ['foo','bar','baz']} 63 | 64 | - [doc] Drawer: add examples. 65 | 66 | 67 | 0.552 2020-09-11 Released-By: PERLANCAR; Urgency: medium 68 | 69 | - Org::Element::Setting: provide raw_arg() method. 70 | 71 | 72 | 0.551 2020-04-01 Released-By: PERLANCAR; Urgency: medium 73 | 74 | [Removed] 75 | 76 | - Split Org::Dump to its own distribution. 77 | 78 | 79 | 0.550 2019-02-08 Released-By: PERLANCAR 80 | 81 | - [bugfix][compliance] Make weekday name in timestamp optional when 82 | there's time of day (e.g. [2019-02-08 11:05] instead of [2019-02-08 83 | Fri 11:05]) [RT#128450]. 84 | 85 | 86 | 0.54 2017-07-10 Released-By: PERLANCAR 87 | 88 | - No functional changes. 89 | 90 | - Replace Log::Any with Log::ger. 91 | 92 | 93 | 0.53 2016-12-24 Released-By: PERLANCAR 94 | 95 | - Add methods is_block & is_inline to make it easy to check whether 96 | an element is a 'blocky' element or an 'inline' element. 97 | 98 | 99 | 0.52 2016-11-06 Released-By: PERLANCAR 100 | 101 | - Use $] >= ... instead of $] ge ... to avoid test failures on 102 | FreeBSD on 5.20.x perls [suggested by Slaven Rezic, RT#118647]. 103 | 104 | 105 | 0.51 2016-11-06 Released-By: PERLANCAR 106 | 107 | - Shuts up warning about problematic locale in 5.22 (Karl Williamson). 108 | 109 | - Add document attribute/parser option: ignore_unknown_settings. 110 | 111 | 112 | 0.50 2016-10-02 Released-By: PERLANCAR 113 | 114 | - When we fail getting cached document (e.g. Storable document got 115 | truncated, etc), instead of dying we continue to skip the cache 116 | and reparse the original document. 117 | 118 | 119 | 0.49 2016-07-14 Released-By: PERLANCAR 120 | 121 | - [Bugfix] Fix stringification of Setting element. 122 | 123 | 124 | 0.48 2016-07-14 Released-By: PERLANCAR 125 | 126 | - [Bugfix][internal] walk(): Copy children to a temporary array first, 127 | so that in the event that a child in the middle is removed during 128 | walk, the entire children are still walked into. 129 | 130 | - [Incompatible change] Rename progress() to statistics_cookie(). 131 | 132 | - Compliance: recognize COMMENT keyword in a headline. 133 | 134 | - Compliance: comment line is allowed to be indented. 135 | 136 | 137 | 0.47 2016-04-01 Released-By: PERLANCAR 138 | 139 | - parse_file(): Add option 'cache' to turn on caching (the old 140 | 'cache_file' is replaced by this option; the 'cache' option selects a 141 | default cache dir in ~/.cache/perl-org-parser/ to make things more 142 | transparent and caching easier). Caching can also be enabled (or 143 | disabled) by setting PERL_ORG_PARSER_CACHE=1 (or 0). 144 | 145 | 146 | 0.46 2016-03-24 Released-By: PERLANCAR 147 | 148 | - walk(): Pass 2nd argument to code: level (currently undocumented). 149 | Currently this is used by the `stat-org-document` script to get tree 150 | depth and number of elements at each level. 151 | 152 | - Replace File::Slurp::Tiny with File::Slurper. 153 | 154 | 155 | 0.45 2015-08-18 Released-By: PERLANCAR 156 | 157 | - No functional changes. 158 | 159 | - Switch from Log::Any to Log::Any::IfLOG. 160 | 161 | 162 | 0.44 2015-03-14 Released-By: PERLANCAR 163 | 164 | - Add cmp_priorities() method to Org::Document. 165 | 166 | - Add headlines() method to Org::Element. 167 | 168 | 169 | 0.43 2014-12-01 Released-By: PERLANCAR 170 | 171 | - No functional changes. 172 | 173 | - [Bugfix] Fix logic in headline's get_property(), the previous fix 174 | didn't really fix the problem [RT#100553]. 175 | 176 | 177 | 0.42 2014-11-26 Released-By: PERLANCAR 178 | 179 | - Fix get_property()'s search_parent argument (was not properly 180 | implemented) [RT#100553]. 181 | 182 | 183 | 0.41 2014-11-18 Released-By: PERLANCAR 184 | 185 | - Add role: Org::Element::InlineRole, which adds the 'as_text' method to 186 | get "rendered plaintext" representation of element. This is mostly 187 | identical to what 'as_string' returns, except for links in which case 188 | 'as_text' will return the description or the link instead of the raw 189 | representation [RT#100396]. 190 | 191 | - Fix documentation [RT#100395]. 192 | 193 | - Add some known settings [RT#100394]. 194 | 195 | 196 | 0.40 2014-07-17 Released-By: PERLANCAR 197 | 198 | - [Bugfix] miscached todo keywords and priorities from previous parse 199 | due to the use of /o regex modifier [RT#98375]. 200 | 201 | 202 | 0.39 2014-07-17 Released-By: SHARYANTO 203 | 204 | [ENHANCEMENTS] 205 | 206 | - Support parsing progress cookie in headline (e.g. '* TODO title [5/10]'). 207 | 208 | - Accept ( and { to start markup, like Emacs (but { hasn't worked yet) 209 | [RT#95947]. 210 | 211 | 212 | [BUG FIXES] 213 | 214 | - Non-TODO headlines can have priority too, so the 'todo_priority' 215 | attribute is renamed to 'priority' [RT#95947](2014-07-16). The old 216 | name is deprecated and will be supported for a while before it is 217 | removed. 218 | 219 | [DOCUMENTATION] 220 | 221 | - Reduce confusion about array/list/arrayref [RT#97244]. 222 | 223 | 224 | 0.38 2014-05-17 Released-By: SHARYANTO 225 | 226 | - Update timestamp parsing to follow newer org-mode ([2014-01-06] is 227 | allowed as well as [2014-01-06 ]). 228 | 229 | - Replace File::Slurp with File::Slurp::Tiny. 230 | 231 | 232 | 0.37 2013-10-27 Released-By: SHARYANTO 233 | 234 | - Support parsing habit-style repeater (thanks Alex White). 235 | 236 | 237 | 0.36 2013-09-23 Released-By: SHARYANTO 238 | 239 | - No functional changes. Update test due to updated timezone database 240 | (WIT -> WIB) [CT]. 241 | 242 | 243 | 0.35 2013-09-05 Released-By: SHARYANTO 244 | 245 | - Text below a list item, indented at the same level as or less than the 246 | list item, can now break the list. This is the behavior of newer 247 | Org-mode (7.x?) and the one that makes more sense. Thanks to Trent 248 | Fisher for finding out about the issue and providing a test case. 249 | 250 | - List: add items(). 251 | 252 | 253 | 0.34 2013-06-28 Released-By: SHARYANTO 254 | 255 | - Add some more known settings from the Orgmode manual's index. 256 | 257 | 258 | 0.33 2013-06-26 Released-By: SHARYANTO 259 | 260 | - No functional changes. Second attempt: add 'use experimental 261 | "smartmatch"' must be given after 'use Moo'. 262 | 263 | 264 | 0.32 2013-06-25 Released-By: SHARYANTO 265 | 266 | - No functional changes. Add 'use experimental "smartmatch"' for 5.18+. 267 | 268 | 269 | 0.31 2013-05-15 Released-By: SHARYANTO 270 | 271 | [BUG FIXES] 272 | 273 | - ListItem: dumping description list item didn't dump the description 274 | term and the " ::" string. 275 | 276 | 277 | 0.30 2013-02-06 Released-By: SHARYANTO 278 | 279 | [ENHANCEMENTS] 280 | 281 | - Headline: Add get_drawer() (implemented by Meng Weng Wong). 282 | 283 | 284 | 0.29 2013-01-15 Released-By: SHARYANTO 285 | 286 | [INCOMPATIBLE CHANGES] 287 | 288 | - Move get_property() from Org::Element to Org::Element::Headline and 289 | change it so it searches children instead of siblings. This is the 290 | correct behavior, as properties are associated with entries/headlines 291 | and not any element. Ref: 292 | http://orgmode.org/manual/Properties-and-Columns.html [RT#82658]. 293 | Thanks Meng Weng Wong. 294 | 295 | 296 | 0.28 2013-01-07 Released-By: SHARYANTO; Message: Happy new year! My first release in 2013 297 | 298 | - No functional changes. 299 | 300 | [BUG FIXES] 301 | 302 | - Correct as_string() for link [RT#82334] 303 | 304 | 305 | 0.27 2012-08-07 Released-By: SHARYANTO 306 | 307 | - No functional changes. Increase Perl minimum version requirement from 308 | 5.10.0 to 5.10.1 due to failing reports in 5.10.0 [RT#78795]. Thanks, 309 | Andreas. 310 | 311 | 312 | 0.26 2012-07-23 Released-By: SHARYANTO 313 | 314 | [ENHANCEMENTS] 315 | 316 | - Org::Parser: Add 'cache_file' option to parse_file(). Caching is done 317 | by storing the parsed Org::Document object using Storable. The 318 | storable file will be used as long as the original Org file is not 319 | modified. It is useful if you often parse a less-often-modified, 320 | largish Org file. 321 | 322 | - Org::Element: Make walk() able to walk headline's title. 323 | 324 | - Org::Element::Timestamp: Add clear_parse_result() to make timestamp 325 | object serializable. 326 | 327 | 328 | 0.25 2012-07-21 Released-By: SHARYANTO 329 | 330 | - No functional changes. Add Unicode test files. 331 | 332 | 333 | 0.24 2012-07-17 Released-By: SHARYANTO 334 | 335 | - Add binmode => ':utf8' flag to read_file() [RT#78423]. 336 | 337 | 338 | 0.23 2012-04-14 Released-By: SHARYANTO 339 | 340 | - No functional changes. Another increase in parsing speed by avoiding 341 | doing unnecessary stuffs in first pass and adding m//o flag. A speedup 342 | of about 1.25x is expected. 343 | 344 | 345 | 0.22 2012-04-13 Released-By: SHARYANTO 346 | 347 | - No functional changes. Faster parsing (reduce overheads from logging 348 | statements and %+ access). A speedup of about 2x is expected. 349 | 350 | 351 | 0.21 2011-09-23 Released-By: SHARYANTO 352 | 353 | - No functional changes. Remove debug message. 354 | 355 | 356 | 0.20 2011-09-23 Released-By: SHARYANTO 357 | 358 | [ENHANCEMENTS] 359 | 360 | - Allow setting time zone (for timestamps). 361 | 362 | 363 | 0.19 2011-09-22 Released-By: SHARYANTO 364 | 365 | [ENHANCEMENTS] 366 | 367 | - Table: Add as_aoa(). 368 | 369 | - Table row: Add as_array(). 370 | 371 | 372 | 0.18 2011-08-11 Released-By: SHARYANTO 373 | 374 | [INCOMPATIBLE CHANGES] 375 | 376 | - Rename Org::Element::ShortExample to Org::Element::FixedWidthSection. 377 | 378 | - Allow /^\s*:$/ line as a special case in fixed width section (ref: 379 | [org-mode feb52f9028e73f0f49390780bb2e61cc9da04303]) 380 | 381 | 382 | 0.17 2011-07-27 Released-By: SHARYANTO 383 | 384 | [INCOMPATIBLE CHANGES] 385 | 386 | - Rename Org::Element::Base to Org::Element. 387 | 388 | [ENHANCEMENTS] 389 | 390 | - Allow decimal fraction on timestamp repeater & warning period. 391 | 392 | - Base: add method remove(). 393 | 394 | - Headline: add methods {promote,demote}_{node,branch}(). 395 | 396 | 397 | 0.16 2011-06-16 Released-By: SHARYANTO 398 | 399 | - Relax timestamp parsing for Chinese/French timestamps. 400 | 401 | 402 | 0.15 2011-06-09 Released-By: SHARYANTO 403 | 404 | [REMOVED] 405 | 406 | - dump-org-structure script moved to App::OrgUtils. 407 | 408 | [ENHANCEMENTS] 409 | 410 | - Base: Add field_name(). 411 | 412 | - Headline: Add is_leaf(). 413 | 414 | 415 | 0.14 2011-06-06 Released-By: SHARYANTO 416 | 417 | [ENHANCEMENTS] 418 | 419 | - Headline: Add get_active_timestamp(). 420 | 421 | 422 | 0.13 2011-06-06 Released-By: SHARYANTO 423 | 424 | - No functional changes for the parser. 425 | 426 | [REMOVED] 427 | 428 | - Spin off 'remind-due-todos' script into App::ListOrgHeadlines. 429 | 430 | 431 | 0.12 2011-05-25 Released-By: SHARYANTO 432 | 433 | [ENHANCEMENTS] 434 | 435 | - Compliance: Parse .+/++ repeater forms and warning period in timestamp 436 | [thanks Louis B. Moore] 437 | 438 | [BUG FIXES] 439 | 440 | - Fix regex for parsing table [RT#68442, thanks Slaven Rezic] 441 | 442 | [ETC] 443 | 444 | - Use utf8 in dump-org-structure script. 445 | 446 | 447 | 0.11 2011-05-23 Released-By: SHARYANTO 448 | 449 | [ENHANCEMENTS] 450 | 451 | - Compliance: blocks can be indented. 452 | 453 | - Compliance: some settings can be indented. 454 | 455 | - Parse short example (one-line literal example with colon+space prefix 456 | syntax). See Org::Element::ShortExample. 457 | 458 | 459 | 0.10 2011-04-21 Released-By: SHARYANTO 460 | 461 | [FIXES] 462 | 463 | - Fixes to POD documentation. 464 | 465 | - More specific regex for tag. 466 | 467 | [ETC] 468 | 469 | - Update todo.org Released-By: some questions cleared up by Carsten Dominik 470 | 471 | 472 | 0.09 2011-03-31 Released-By: SHARYANTO 473 | 474 | [FIXES] 475 | 476 | - Fix SYNOPSIS, use a slightly more complex Org document example. 477 | 478 | 479 | 0.08 2011-03-23 Released-By: SHARYANTO 480 | 481 | [FIXES] 482 | 483 | - Update bin/remind-due-todos. 484 | 485 | 486 | 0.07 2011-03-23 Released-By: SHARYANTO 487 | 488 | [ENHANCEMENTS] 489 | 490 | - Org::Element::Base: add find(), walk_parents(), headline() 491 | 492 | - Org::Element::Headline: add get_tags() 493 | 494 | 495 | [FIXES] 496 | 497 | - Link description can contain markups. 498 | 499 | 500 | 0.06 2011-03-23 Released-By: SHARYANTO 501 | 502 | [FIXES] 503 | 504 | - Some regex fixes. 505 | 506 | 507 | 0.05 2011-03-23 Released-By: SHARYANTO 508 | 509 | [INCOMPATIBLE CHANGES] 510 | 511 | - Org::Element::TimeRange: datetime1 & datetime2 attributes removed, 512 | replaced with ts1 & ts2 (timestamp elements). 513 | 514 | [ENHANCEMENTS] 515 | 516 | - Parses event duration and repeater interval in timestamps. 517 | 518 | 519 | 0.04 2011-03-22 Released-By: SHARYANTO 520 | 521 | - This release is a major refactoring from the previous one. 522 | 523 | [INCOMPATIBLE CHANGES] 524 | 525 | - Org::Parser: handler() removed, use Org::Document's walk() instead. 526 | 527 | - Refactoring: some classes removed/merged, some added. 528 | 529 | [ENHANCEMENTS] 530 | 531 | - Dual-pass parsing for more correct behaviour. 532 | 533 | - Parse link, plain list (including ordered/unordered/description list), 534 | target, radio target, comment, footnote. 535 | 536 | - Add a couple of utility methods in Element::Base: seniority(), 537 | prev_sibling(), next_sibling(), walk(), get_property(). 538 | 539 | [ETC] 540 | 541 | - Project todo list now in distribution's todo.org 542 | 543 | 544 | 0.03 2011-03-18 Released-By: SHARYANTO 545 | 546 | [ENHANCEMENTS] 547 | 548 | - Parse text markups (bold, italic, etc). 549 | 550 | - bin/dump-org-structure outputs nicer format. 551 | 552 | [FIXES] 553 | 554 | - Todo keyword can also be separated from title with \W (not just \s), 555 | e.g. '* TODO/quit smoking'. 556 | 557 | 558 | 0.02 2011-03-17 Released-By: SHARYANTO 559 | 560 | [INCOMPATIBLE CHANGES] 561 | 562 | - Refactoring: parser now returns Org::Document instance, which contains 563 | Org::Element instances. handler sub parameter changed. 564 | 565 | [ENHANCEMENTS] 566 | 567 | - Parse tables. 568 | 569 | - Headline titles can now contain inline elements (normal text as well 570 | as other elements, such as timestamps, etc). 571 | 572 | - Add another example script: dump-org-structure. 573 | 574 | - Recognize blocks: HTML, LATEX, ASCII. 575 | 576 | [FIXES] 577 | 578 | - Setting/block/drawer/property names are case-insensitive. 579 | 580 | 581 | 0.01 2011-03-16 Released-By: SHARYANTO 582 | 583 | - First release. 584 | -------------------------------------------------------------------------------- /devnotes.org: -------------------------------------------------------------------------------- 1 | 2 | - log :: 3 | + [2023-08-05 Sat] :: "Build with PERL_POD_WEAVER_PLUGIN_RINCI_FORCE_RELOAD 4 | set to 0 since reloading Org/Document.pm will result in Moo error." this no 5 | longer works, so currently I use the Pod::Weaver bundle 6 | Author::PERLANCAR::NoRinci. 7 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | ;--------------------------------- 2 | author = perlancar 3 | copyright_holder = perlancar 4 | license = Perl_5 5 | ;--------------------------------- 6 | 7 | version = 0.561 8 | 9 | name = Org-Parser 10 | 11 | [@Author::PERLANCAR] 12 | :version=0.606 13 | 14 | [Prereqs / TestRequires] 15 | lib=0 16 | File::Temp=0.2307 17 | FindBin=0 18 | Module::Loaded=0 19 | Org::Dump=0.551 20 | Test::Exception=0 21 | Test::More=0.98 22 | 23 | [Prereqs] 24 | perl= 5.014 25 | if=0 26 | locale=0 27 | strict=0 28 | utf8=0 29 | warnings=0 30 | Cwd=0 31 | DateTime= 0 32 | DateTime::Event::Recurrence=0 33 | Digest::MD5=0 34 | File::Slurper=0 35 | List::MoreUtils=0 36 | Log::ger=0.038 37 | Module::List=0 38 | Module::Load=0 39 | Moo=0 40 | Moo::Role=0 41 | Scalar::Util=0 42 | Storable=3.08 43 | Time::HiRes=0 44 | 45 | [Acme::CPANModules::Whitelist] 46 | module=List::MoreUtils 47 | -------------------------------------------------------------------------------- /lib/Org/Document.pm: -------------------------------------------------------------------------------- 1 | package Org::Document; 2 | 3 | use 5.010001; 4 | use locale; 5 | use Log::ger; 6 | use Moo; 7 | no if $] >= 5.021_006, warnings => "locale"; 8 | extends 'Org::Element'; 9 | 10 | use List::MoreUtils qw(firstidx); 11 | use Time::HiRes qw(gettimeofday tv_interval); 12 | 13 | # AUTHORITY 14 | # DATE 15 | # DIST 16 | # VERSION 17 | 18 | has _srclabel => (is => 'rw'); 19 | has tags => (is => 'rw'); 20 | has todo_states => (is => 'rw'); 21 | has done_states => (is => 'rw'); 22 | has priorities => (is => 'rw'); 23 | has drawer_names => (is => 'rw'); 24 | has properties => (is => 'rw'); 25 | has radio_targets => (is => 'rw'); 26 | 27 | has time_zone => (is => 'rw'); 28 | 29 | has ignore_unknown_settings => (is => 'rw'); 30 | 31 | our $tags_re = qr/:(?:[\w@]+:)+/u; 32 | my $ls_re = qr/(?:(?<=[\015\012])|\A)/; # line start 33 | my $le_re = qr/(?:\R|\z)/; # line end 34 | our $arg_re = qr/(?: '(? [^']*)' | 35 | "(? [^"]*)" | 36 | (? \S+) ) 37 | /x; 38 | our $args_re = qr/(?: $arg_re (?:[ \t]+ $arg_re)*)/x; 39 | my $tstamp_re = qr/(?:\[\d{4}-\d{2}-\d{2} [^\n\]]*\])/x; 40 | my $act_tstamp_re = qr/(?: <\d{4}-\d{2}-\d{2} [^\n>]* >)/x; 41 | my $fn_name_re = qr/(?:[^ \t\n:\]]+)/x; 42 | my $text_re = 43 | qr{ 44 | (? \[\[(? [^\]\n]+)\] 45 | (?:\[(? (?:[^\]]|\R)+)\])?\]) | 46 | (? <<<(? [^>\n]+)>>>) | 47 | (? <<(? [^>\n]+)>>) | 48 | 49 | # timestamp & time range 50 | (? (? $tstamp_re)-- 51 | (? $tstamp_re)) | 52 | (? $tstamp_re) | 53 | (? (? $act_tstamp_re)-- 54 | (? $act_tstamp_re)) | 55 | (? $act_tstamp_re) | 56 | 57 | # footnote (num, name + def, name + inline definition) 58 | (? \[(?\d+)\]) | 59 | (? $ls_re \[fn:(? $fn_name_re)\] 60 | [ \t]* (? [^ \t\n]+)) | 61 | (? \[fn:(? $fn_name_re?):? 62 | (? ([^\n\]]+)?)\]) | 63 | 64 | (? (?:(?<=\s|\(|\{)|\A) # whitespace, open paren, open curly paren 65 | [*/+=~_] 66 | (?=\S)) | 67 | (? (?<=\S) 68 | [*/+=~_] 69 | # actually emacs doesn't allow ! after markup 70 | (?:(?=[ \t\n:;"',.!?\)*-])|\z)) | 71 | 72 | (? (?:[^\[<*/+=~_\n]+|.+?)) 73 | #(? .+?) # too dispersy 74 | }sxi; 75 | 76 | # XXX parser must be fixed: block elements have different precedence instead of 77 | # flat like this. a headline has the highest precedence and a block or a drawer 78 | # cannot contain a headline (e.g. "#+BEGIN_SRC foo\n* header\n#+END_SRC" should 79 | # not contain a literal "* header" text but that is a headline. currently, a 80 | # block or a drawer swallows a headline. 81 | 82 | my $block_elems_re = # top level elements 83 | qr/(? $ls_re (?[ \t]*) 84 | \#\+BEGIN_(?\w+) 85 | (?:[ \t]+(?[^\n]*))?\R 86 | (?(?:.|\R)*?) 87 | \R(?[ \t]*) 88 | \#\+END_\k $le_re) | 89 | (? $ls_re (?[ \t]*) \#\+ 90 | (? \w+): (?: [ \t]+ 91 | (? [^\n]*))? $le_re) | 92 | (? (?: $ls_re [ \t]* (?::[ ][^\n]* | :$) $le_re )+ ) | 93 | (? $ls_re [ \t]*\#[^\n]*(?:\R\#[^\n]*)* (?:\R|\z)) | 94 | (? $ls_re (?\*+) [ \t] 95 | (?[^\n]*?) 96 | (?:[ \t]+(? $tags_re))?[ \t]* $le_re) | 97 | (? $ls_re (?[ \t]*) 98 | (?[+*-]|\d+\.) [ \t]+ 99 | (? \[(? [ X-])\])? 100 | (?: (? [^\n]+?) [ ]::)?) | 101 | (? (?: $ls_re [ \t]* \| [ \t]* \S[^\n]* $le_re)+) | 102 | (? $ls_re [ \t]* :(? \w+): [ \t]*\R 103 | (?(?:.|\R)*?) 104 | $ls_re [ \t]* :END:) | 105 | (? (?:[^#|:+*0-9\n-]+|\n+|.)+?) 106 | #(? .+?) # too dispersy 107 | /msxi; 108 | 109 | sub _init_pass1 { 110 | my ($self) = @_; 111 | $self->tags([]); 112 | $self->todo_states([]); 113 | $self->done_states([]); 114 | $self->priorities([]); 115 | $self->properties({}); 116 | $self->drawer_names([qw/CLOCK LOGBOOK PROPERTIES/]); 117 | # FEEDSTATUS 118 | $self->radio_targets([]); 119 | } 120 | 121 | sub _init_pass2 { 122 | my ($self) = @_; 123 | if (!@{ $self->todo_states } && !@{ $self->done_states }) { 124 | $self->todo_states(['TODO']); 125 | $self->done_states(['DONE']); 126 | } 127 | if (!@{ $self->priorities }) { 128 | $self->priorities([qw/A B C/]); 129 | } 130 | $self->children([]); 131 | } 132 | 133 | sub __parse_args { 134 | my $args = shift; 135 | return [] unless defined($args) && length($args); 136 | #$log->tracef("args = %s", $args); 137 | my @args; 138 | while ($args =~ /$arg_re (?:\s+|\z)/xg) { 139 | if (defined $+{squote}) { 140 | push @args, $+{squote}; 141 | } elsif (defined $+{dquote}) { 142 | push @args, $+{dquote}; 143 | } else { 144 | push @args, $+{bare}; 145 | } 146 | } 147 | #$log->tracef("\\\@args = %s", \@args); 148 | \@args; 149 | } 150 | 151 | sub __format_args { 152 | my ($args) = @_; 153 | my @s; 154 | for (@$args) { 155 | if (/\A(?:[A-Za-z0-9_:-]+|\|)\z/) { 156 | push @s, $_; 157 | } elsif (/"/) { 158 | push @s, qq('$_'); 159 | } else { 160 | push @s, qq("$_"); 161 | } 162 | } 163 | join " ", @s; 164 | } 165 | 166 | sub BUILD { 167 | my ($self, $args) = @_; 168 | $self->document($self) unless $self->document; 169 | 170 | if (defined $args->{from_string}) { 171 | 172 | # NOTE: parsing is done twice. first pass will set settings (e.g. custom 173 | # todo keywords set by #+TODO), scan for radio targets. after that we 174 | # scan again to build the elements tree. 175 | 176 | $self->_init_pass1(); 177 | $self->_parse($args->{from_string}, 1); 178 | $self->_init_pass2(); 179 | $self->_parse($args->{from_string}, 2); 180 | } 181 | } 182 | 183 | # parse blocky elements: setting, blocks, headline, drawer 184 | sub _parse { 185 | my ($self, $str, $pass) = @_; 186 | log_trace('-> _parse(%s, pass=%d)', $str, $pass); 187 | my $t0 = [gettimeofday]; 188 | 189 | my $last_el; 190 | 191 | my $last_headline; 192 | my $last_headlines = [$self]; # [$doc, $last_hl_level1, $last_hl_lvl2, ...] 193 | my $last_listitem; 194 | my $last_lists = []; # [last_List_obj_for_indent_level0, ...] 195 | my $parent; 196 | 197 | my @text; 198 | while ($str =~ /$block_elems_re/og) { 199 | $parent = $last_listitem // $last_headline // $self; 200 | #$log->tracef("TMP: parent=%s (%s)", ref($parent), $parent->_str); 201 | my %m = %+; 202 | next unless keys %m; # perlre bug? 203 | #if ($log->is_trace) { 204 | # # profiler shows that this is very heavy, so commenting this out 205 | # $log->tracef("TMP: match block element: %s", \%+) if $pass==2; 206 | #} 207 | 208 | if (defined $m{text}) { 209 | push @text, $m{text}; 210 | next; 211 | } else { 212 | if (@text) { 213 | my $text = join("", @text); 214 | if ($last_el && $last_el->isa('Org::Element::ListItem')) { 215 | # a list is broken by either: a) another list (where the 216 | # bullet type or indent is different; handled in the 217 | # handling of $m{li_header}) or b) by two blank lines, or c) 218 | # by non-blank text that is indented less than or equal to 219 | # the last list item's indent. 220 | 221 | # a single blank line does not break a list. a text that is 222 | # more indented than the last list item's indent will become 223 | # the child of that list item. 224 | 225 | my ($firstline, $restlines) = $text =~ /(.*?\r?\n)(.+)/s; 226 | if ($restlines) { 227 | $restlines =~ /\A([ \t]*)/; 228 | my $restlineslevel = length($1); 229 | my $listlevel = length($last_el->parent->indent); 230 | if ($restlineslevel <= $listlevel) { 231 | my $origparent = $parent; 232 | # find lesser-indented list 233 | $parent = $last_headline // $self; 234 | for (my $i=$restlineslevel-1; $i>=0; $i--) { 235 | if ($last_lists->[$i]) { 236 | $parent = $last_lists->[$i]; 237 | last; 238 | } 239 | } 240 | splice @$last_lists, $restlineslevel; 241 | $self->_add_text($firstline, $origparent, $pass); 242 | $self->_add_text($restlines, $parent, $pass); 243 | goto SKIP1; 244 | } 245 | } 246 | } 247 | $self->_add_text($text, $parent, $pass); 248 | SKIP1: 249 | @text = (); 250 | $last_el = undef; 251 | } 252 | } 253 | 254 | my $el; 255 | if ($m{block} && $pass == 2) { 256 | 257 | require Org::Element::Block; 258 | $el = Org::Element::Block->new( 259 | _str=>$m{block}, 260 | document=>$self, parent=>$parent, 261 | begin_indent=>$m{block_begin_indent}, 262 | end_indent=>$m{block_end_indent}, 263 | name=>$m{block_name}, args=>__parse_args($m{block_raw_arg}), 264 | raw_content=>$m{block_content}, 265 | ); 266 | 267 | } elsif ($m{setting}) { 268 | 269 | require Org::Element::Setting; 270 | my $uc_setting_name = uc($m{setting_name}); 271 | if ($m{setting_indent} && 272 | !(grep { $_ eq $uc_setting_name } 273 | @{Org::Element::Setting->indentable_settings})) { 274 | push @text, $m{setting}; 275 | next; 276 | } else { 277 | $el = Org::Element::Setting->new( 278 | pass => $pass, 279 | _str=>$m{setting}, 280 | document=>$self, parent=>$parent, 281 | indent => $m{setting_indent}, 282 | name=>$m{setting_name}, 283 | raw_arg => $m{setting_raw_arg}, 284 | args=>__parse_args($m{setting_raw_arg}), 285 | ); 286 | } 287 | 288 | } elsif ($m{fixedw} && $pass == 2) { 289 | 290 | require Org::Element::FixedWidthSection; 291 | $el = Org::Element::FixedWidthSection->new( 292 | pass => $pass, 293 | _str=>$m{fixedw}, 294 | document=>$self, parent=>$parent, 295 | ); 296 | 297 | } elsif ($m{comment} && $pass == 2) { 298 | 299 | require Org::Element::Comment; 300 | $el = Org::Element::Comment->new( 301 | _str=>$m{comment}, 302 | document=>$self, parent=>$parent, 303 | ); 304 | 305 | } elsif ($m{table} && $pass == 2) { 306 | 307 | require Org::Element::Table; 308 | $el = Org::Element::Table->new( 309 | pass=>$pass, 310 | _str=>$m{table}, 311 | document=>$self, parent=>$parent, 312 | ); 313 | 314 | } elsif ($m{drawer} && $pass == 2) { 315 | 316 | require Org::Element::Drawer; 317 | my $raw_content = $m{drawer_content}; 318 | $el = Org::Element::Drawer->new( 319 | document=>$self, parent=>$parent, 320 | name => uc($m{drawer_name}), pass => $pass, 321 | ); 322 | $self->_add_text($raw_content, $el, $pass); 323 | 324 | # for properties, we also parse property lines from raw drawer 325 | # content. this is currently separate from normal Org text parsing, 326 | # i'm not clear yet on how to do this canonically. 327 | $el->_parse_properties($raw_content); 328 | 329 | } elsif ($m{li_header} && $pass == 2) { 330 | 331 | require Org::Element::List; 332 | require Org::Element::ListItem; 333 | 334 | my $level = length($m{li_indent}); 335 | my $bullet = $m{li_bullet}; 336 | my $indent = $m{li_indent}; 337 | my $dt = $m{li_dt}; 338 | my $cbstate = $m{li_cbstate}; 339 | my $type = defined($dt) ? 'D' : 340 | $bullet =~ /^\d+\./ ? 'O' : 'U'; 341 | my $bstyle = $type eq 'O' ? '.' : $bullet; 342 | 343 | # parent for list is the last listitem of a lesser-indented list (or 344 | # last headline, or document) 345 | $parent = $last_headline // $self; 346 | for (my $i=$level-1; $i>=0; $i--) { 347 | if ($last_lists->[$i]) { 348 | $parent = $last_lists->[$i]->children->[-1]; 349 | last; 350 | } 351 | } 352 | 353 | my $list = $last_lists->[$level]; 354 | if (!$list || $list->type ne $type || 355 | $list->bullet_style ne $bstyle) { 356 | $list = Org::Element::List->new( 357 | document => $self, parent => $parent, 358 | indent=>$indent, type=>$type, bullet_style=>$bstyle, 359 | ); 360 | $last_lists->[$level] = $list; 361 | $parent->children([]) if !$parent->children; 362 | push @{ $parent->children }, $list; 363 | } 364 | $last_lists->[$level] = $list; 365 | 366 | # parent for list item is list 367 | $parent = $list; 368 | 369 | $el = Org::Element::ListItem->new( 370 | document=>$self, parent=>$list, 371 | indent=>$indent, bullet=>$bullet); 372 | $el->check_state($cbstate) if $cbstate; 373 | $el->desc_term($self->_add_text_container($dt, $list, $pass)) 374 | if defined($dt); 375 | 376 | splice @$last_lists, $level+1; 377 | $last_listitem = $el; 378 | 379 | } elsif ($m{headline} && $pass == 2) { 380 | 381 | require Org::Element::Headline; 382 | my $level = length $m{h_bullet}; 383 | 384 | # parent is upper-level headline 385 | $parent = undef; 386 | for (my $i=$level-1; $i>=0; $i--) { 387 | $parent = $last_headlines->[$i] and last; 388 | } 389 | $parent //= $self; 390 | 391 | $el = Org::Element::Headline->new( 392 | _str=>$m{headline}, 393 | document=>$self, parent=>$parent, 394 | level=>$level, 395 | ); 396 | $el->tags(__split_tags($m{h_tags})) if ($m{h_tags}); 397 | my $title = $m{h_title}; 398 | 399 | # recognize todo keyword 400 | my $todo_kw_re = "(?:". 401 | join("|", map {quotemeta} 402 | "COMMENT", 403 | @{$self->todo_states}, @{$self->done_states}) . ")"; 404 | if ($title =~ s/^($todo_kw_re)(\s+|\W)/$2/) { 405 | my $state = $1; 406 | $title =~ s/^\s+//; 407 | $el->is_todo(1); 408 | $el->todo_state($state); 409 | $el->is_done((grep { $_ eq $state } @{ $self->done_states }) ? 1:0); 410 | } 411 | 412 | # recognize priority cookie 413 | my $prio_re = "(?:". 414 | join("|", map {quotemeta} @{$self->priorities}) . ")"; 415 | if ($title =~ s/\[#($prio_re)\]\s*//) { 416 | $el->priority($1); 417 | } 418 | 419 | # recognize statistics cookie 420 | if ($title =~ s!\[(\d+%|\d+/\d+)\]\s*!!o) { 421 | $el->statistics_cookie($1); 422 | } 423 | 424 | $el->title($self->_add_text_container($title, $parent, $pass)); 425 | 426 | $last_headlines->[$el->level] = $el; 427 | splice @$last_headlines, $el->level+1; 428 | $last_headline = $el; 429 | $last_listitem = undef; 430 | $last_lists = []; 431 | } 432 | 433 | # we haven't caught other matches to become element 434 | die "BUG1: no element" unless $el || $pass != 2; 435 | 436 | $parent->children([]) if !$parent->children; 437 | push @{ $parent->children }, $el; 438 | $last_el = $el; 439 | } 440 | 441 | # remaining text 442 | if (@text) { 443 | $self->_add_text(join("", @text), $parent, $pass); 444 | } 445 | @text = (); 446 | 447 | log_trace('<- _parse(), elapsed time=%.3fs', 448 | tv_interval($t0, [gettimeofday])); 449 | } 450 | 451 | sub _add_text_container { 452 | require Org::Element::Text; 453 | my ($self, $str, $parent, $pass) = @_; 454 | my $container = Org::Element::Text->new( 455 | document=>$self, parent=>$parent, 456 | text=>'', style=>'', 457 | ); 458 | $self->_add_text($str, $container, $pass); 459 | $container = $container->children->[0] if 460 | $container->children && @{$container->children} == 1 && 461 | $container->children->[0]->isa('Org::Element::Text'); 462 | $container; 463 | } 464 | 465 | sub _add_text { 466 | require Org::Element::Text; 467 | my ($self, $str, $parent, $pass) = @_; 468 | $parent //= $self; 469 | #$log->tracef("-> _add_text(%s, pass=%d)", $str, $pass); 470 | 471 | my @plain_text; 472 | while ($str =~ /$text_re/og) { 473 | my %m = %+; 474 | #if ($log->is_trace) { 475 | # # profiler shows that this is very heavy, so commenting this out 476 | # $log->tracef("TMP: match text: %s", \%+); 477 | #} 478 | my $el; 479 | 480 | if (defined $m{plain_text} && $pass == 2) { 481 | push @plain_text, $m{plain_text}; 482 | next; 483 | } else { 484 | if (@plain_text) { 485 | $self->_add_plain_text(join("", @plain_text), $parent, $pass); 486 | @plain_text = (); 487 | } 488 | } 489 | 490 | if ($m{link} && $pass == 2) { 491 | require Org::Element::Link; 492 | $el = Org::Element::Link->new( 493 | document => $self, parent => $parent, 494 | link=>$m{link_link}, 495 | ); 496 | if (defined($m{link_desc}) && length($m{link_desc})) { 497 | $el->description( 498 | $self->_add_text_container($m{link_desc}, 499 | $el, $pass)); 500 | } 501 | } elsif ($m{radio_target}) { 502 | require Org::Element::RadioTarget; 503 | $el = Org::Element::RadioTarget->new( 504 | pass => $pass, 505 | document => $self, parent => $parent, 506 | target=>$m{rt_target}, 507 | ); 508 | } elsif ($m{target} && $pass == 2) { 509 | require Org::Element::Target; 510 | $el = Org::Element::Target->new( 511 | document => $self, parent => $parent, 512 | target=>$m{t_target}, 513 | ); 514 | } elsif ($m{fn_num} && $pass == 2) { 515 | require Org::Element::Footnote; 516 | $el = Org::Element::Footnote->new( 517 | document => $self, parent => $parent, 518 | name=>$m{fn_num_num}, is_ref=>1, 519 | ); 520 | } elsif ($m{fn_namedef} && $pass == 2) { 521 | require Org::Element::Footnote; 522 | $el = Org::Element::Footnote->new( 523 | document => $self, parent => $parent, 524 | name=>$m{fn_namedef_name}, 525 | is_ref=>$m{fn_namedef_def} ? 0:1, 526 | ); 527 | $el->def($self->_add_text_container($m{fn_namedef_def}, 528 | $parent, $pass)); 529 | } elsif ($m{fn_nameidef} && $pass == 2) { 530 | require Org::Element::Footnote; 531 | $el = Org::Element::Footnote->new( 532 | document => $self, parent => $parent, 533 | name=>$m{fn_nameidef_name}, 534 | is_ref=>($m{fn_nameidef_def} ? 0:1) || 535 | !length($m{fn_nameidef_name}), 536 | ); 537 | $el->def(length($m{fn_nameidef_def}) ? 538 | $self->_add_text_container($m{fn_nameidef_def}, 539 | $parent, $pass) : undef); 540 | } elsif ($m{trange} && $pass == 2) { 541 | require Org::Element::TimeRange; 542 | require Org::Element::Timestamp; 543 | $el = Org::Element::TimeRange->new( 544 | document => $self, parent => $parent, 545 | ); 546 | my $opts = {allow_event_duration=>0, allow_repeater=>0}; 547 | $el->ts1(Org::Element::Timestamp->new( 548 | _str=>$m{trange_ts1}, document=>$self, parent=>$parent)); 549 | $el->ts1->_parse_timestamp($m{trange_ts1}, $opts); 550 | $el->ts2(Org::Element::Timestamp->new( 551 | _str=>$m{trange_ts2}, document=>$self, parent=>$parent)); 552 | $el->ts2->_parse_timestamp($m{trange_ts2}, $opts); 553 | $el->children([$el->ts1, $el->ts2]); 554 | } elsif ($m{tstamp} && $pass == 2) { 555 | require Org::Element::Timestamp; 556 | $el = Org::Element::Timestamp->new( 557 | _str => $m{tstamp}, document => $self, parent => $parent, 558 | ); 559 | $el->_parse_timestamp($m{tstamp}); 560 | } elsif ($m{act_trange} && $pass == 2) { 561 | require Org::Element::TimeRange; 562 | require Org::Element::Timestamp; 563 | $el = Org::Element::TimeRange->new( 564 | document => $self, parent => $parent, 565 | ); 566 | my $opts = {allow_event_duration=>0, allow_repeater=>0}; 567 | $el->ts1(Org::Element::Timestamp->new( 568 | _str=>$m{act_trange_ts1}, document=>$self, parent=>$parent)); 569 | $el->ts1->_parse_timestamp($m{act_trange_ts1}, $opts); 570 | $el->ts2(Org::Element::Timestamp->new( 571 | _str=>$m{act_trange_ts2}, document=>$self, parent=>$parent)); 572 | $el->ts2->_parse_timestamp($m{act_trange_ts2}, $opts); 573 | $el->children([$el->ts1, $el->ts2]); 574 | } elsif ($m{act_tstamp} && $pass == 2) { 575 | require Org::Element::Timestamp; 576 | $el = Org::Element::Timestamp->new( 577 | _str => $m{act_tstamp}, document => $self, parent => $parent, 578 | ); 579 | $el->_parse_timestamp($m{act_tstamp}); 580 | } elsif ($m{markup_start} && $pass == 2) { 581 | require Org::Element::Text; 582 | $el = Org::Element::Text->new( 583 | document => $self, parent => $parent, 584 | style=>'', text=>$m{markup_start}, 585 | ); 586 | # temporary mark, we need to apply markup later 587 | $el->{_mu_start}++; 588 | } elsif ($m{markup_end} && $pass == 2) { 589 | require Org::Element::Text; 590 | $el = Org::Element::Text->new( 591 | document => $self, parent => $parent, 592 | style=>'', text=>$m{markup_end}, 593 | ); 594 | # temporary mark, we need to apply markup later 595 | $el->{_mu_end}++; 596 | } 597 | die "BUG2: no element" unless $el || $pass != 2; 598 | $parent->children([]) if !$parent->children; 599 | push @{ $parent->children }, $el; 600 | } 601 | 602 | # remaining text 603 | if (@plain_text && $pass == 2) { 604 | $parent->children([]) if !$parent->children; 605 | push @{$parent->children}, Org::Element::Text->new( 606 | text => join("", @plain_text), style=>'', 607 | document=>$self, parent=>$parent); 608 | @plain_text = (); 609 | } 610 | 611 | if ($pass == 2) { 612 | $self->_apply_markup($parent); 613 | if (@{$self->radio_targets}) { 614 | my $re = join "|", map {quotemeta} @{$self->radio_targets}; 615 | $re = qr/(?:$re)/i; 616 | $self->_linkify_rt_recursive($re, $parent); 617 | } 618 | my $c = $parent->children // []; 619 | } 620 | 621 | #$log->tracef('<- _add_text()'); 622 | } 623 | 624 | # to keep parser's regexes simple and fast, we detect markup in regex rather 625 | # simplistically (as text element) and then apply some more filtering & applying 626 | # logic here 627 | 628 | sub _apply_markup { 629 | #$log->trace("-> _apply_markup()"); 630 | my ($self, $parent) = @_; 631 | my $last_index = 0; 632 | my $c = $parent->children or return; 633 | 634 | while (1) { 635 | #$log->tracef("text cluster = %s", [map {$_->as_string} @$c]); 636 | # find a new mu_start 637 | my $mu_start_index = -1; 638 | my $mu; 639 | for (my $i = $last_index; $i < @$c; $i++) { 640 | next unless $c->[$i]->{_mu_start}; 641 | $mu_start_index = $i; $mu = $c->[$i]->text; 642 | #$log->tracef("found mu_start at %d (%s)", $i, $mu); 643 | last; 644 | } 645 | unless ($mu_start_index >= 0) { 646 | #$log->trace("no more mu_start found"); 647 | last; 648 | } 649 | 650 | # check whether this is a valid markup (has text, has markup end, not 651 | # interspersed with non-text, no more > 1 newlines) 652 | my $mu_end_index = 0; 653 | my $newlines = 0; 654 | my $has_text; 655 | my $has_unmarkable; 656 | for (my $i=$mu_start_index+1; $i < @$c; $i++) { 657 | if ($c->[$i]->isa('Org::Element::Text')) { 658 | $has_text++; 659 | } elsif (1) { 660 | } else { 661 | $has_unmarkable++; last; 662 | } 663 | if ($c->[$i]->{_mu_end} && $c->[$i]->text eq $mu) { 664 | #$log->tracef("found mu_end at %d", $i); 665 | $mu_end_index = $i; last; 666 | } 667 | my $text = $c->[$i]->as_string; 668 | $newlines++ while $text =~ /\R/g; 669 | last if $newlines > 1; 670 | } 671 | my $valid = $has_text && !$has_unmarkable 672 | && $mu_end_index && $newlines <= 1; 673 | #$log->tracef("mu candidate: start=%d, end=%s, ". 674 | # "has_text=%s, has_unmarkable=%s, newlines=%d, valid=%s", 675 | # $mu_start_index, $mu_end_index, 676 | # $has_text, $has_unmarkable, $newlines, $valid 677 | # ); 678 | if ($valid) { 679 | no warnings 'once'; 680 | my $mu_el = Org::Element::Text->new( 681 | document => $self, parent => $parent, 682 | style=>$Org::Element::Text::mu2style{$mu}, text=>'', 683 | ); 684 | my @c2 = splice @$c, $mu_start_index, 685 | $mu_end_index-$mu_start_index+1, $mu_el; 686 | #$log->tracef("grouping %s", [map {$_->text} @c2]); 687 | $mu_el->children(\@c2); 688 | shift @c2; 689 | pop @c2; 690 | for (@c2) { 691 | $_->{parent} = $mu_el; 692 | } 693 | $self->_merge_text_elements(\@c2); 694 | # squish if only one child 695 | if (@c2 == 1) { 696 | $mu_el->text($c2[0]->text); 697 | $mu_el->children(undef); 698 | } 699 | } else { 700 | undef $c->[$mu_start_index]->{_mu_start}; 701 | $last_index++; 702 | } 703 | } 704 | $self->_merge_text_elements($c); 705 | #$log->trace("<- _apply_markup()"); 706 | } 707 | 708 | sub _merge_text_elements { 709 | my ($self, $els) = @_; 710 | #$log->tracef("-> _merge_text_elements(%s)", [map {$_->as_string} @$els]); 711 | return unless @$els >= 2; 712 | my $i=-1; 713 | while (1) { 714 | $i++; 715 | last if $i >= @$els; 716 | next if $els->[$i]->children || !$els->[$i]->isa('Org::Element::Text'); 717 | my $istyle = $els->[$i]->style // ""; 718 | while (1) { 719 | last if $i+1 >= @$els || $els->[$i+1]->children || 720 | !$els->[$i+1]->isa('Org::Element::Text'); 721 | last if ($els->[$i+1]->style // "") ne $istyle; 722 | #$log->tracef("merging text[%d] '%s' with '%s'", 723 | # $i, $els->[$i]->text, $els->[$i+1]->text); 724 | $els->[$i]->{text} .= $els->[$i+1]->{text} // ""; 725 | splice @$els, $i+1, 1; 726 | } 727 | } 728 | #$log->tracef("merge result = %s", [map {$_->as_string} @$els]); 729 | #$log->trace("<- _merge_text_elements()"); 730 | } 731 | 732 | sub _linkify_rt_recursive { 733 | require Org::Element::Text; 734 | require Org::Element::Link; 735 | my ($self, $re, $parent) = @_; 736 | my $c = $parent->children; 737 | return unless $c; 738 | for (my $i=0; $i<@$c; $i++) { 739 | my $el = $c->[$i]; 740 | if ($el->isa('Org::Element::Text')) { 741 | my @split0 = split /\b($re)\b/, $el->text; 742 | next unless @split0 > 1; 743 | my @split; 744 | for my $s (@split0) { 745 | if ($s =~ /^$re$/) { 746 | push @split, Org::Element::Link->new( 747 | document=>$self, parent=>$parent, 748 | link=>$s, description=>undef, 749 | from_radio_target=>1, 750 | ); 751 | } elsif (length $s) { 752 | push @split, Org::Element::Text->new( 753 | document=>$self, parent=>$parent, 754 | text=>$s, style=>$el->style, 755 | ); 756 | } 757 | } 758 | splice @$c, $i, 1, @split; 759 | } 760 | $self->_linkify_rt_recursive($re, $el); 761 | } 762 | } 763 | 764 | sub _add_plain_text { 765 | require Org::Element::Text; 766 | my ($self, $str, $parent, $pass) = @_; 767 | my $el = Org::Element::Text->new( 768 | document=>$self, parent=>$parent, style=>'', text=>$str); 769 | $parent->children([]) if !$parent->children; 770 | push @{ $parent->children }, $el; 771 | } 772 | 773 | sub __split_tags { 774 | [$_[0] =~ /:([^:]+)/g]; 775 | } 776 | 777 | sub load_element_modules { 778 | require Module::List; 779 | require Module::Load; 780 | 781 | my $mm = Module::List::list_modules("Org::Element::", {list_modules=>1}); 782 | for (keys %$mm) { 783 | Module::Load::load($_); 784 | } 785 | } 786 | 787 | sub cmp_priorities { 788 | my ($self, $p1, $p2) = @_; 789 | 790 | my $pp = $self->priorities; 791 | my $pos1 = firstidx {$_ eq $p1} @$pp; 792 | return undef unless $pos1 >= 0; ## no critic: Subroutines::ProhibitExplicitReturnUndef 793 | my $pos2 = firstidx {$_ eq $p2} @$pp; 794 | return undef unless $pos2 >= 0; ## no critic: Subroutines::ProhibitExplicitReturnUndef 795 | $pos1 <=> $pos2; 796 | } 797 | 798 | 1; 799 | # ABSTRACT: Represent an Org document 800 | __END__ 801 | 802 | =head1 SYNOPSIS 803 | 804 | use Org::Document; 805 | 806 | # create a new Org document tree from string 807 | my $org = Org::Document->new(from_string => <. 818 | 819 | 820 | =head1 ATTRIBUTES 821 | 822 | =head2 tags => ARRAY 823 | 824 | List of tags for this file, usually set via #+FILETAGS. 825 | 826 | =head2 todo_states => ARRAY 827 | 828 | List of known (action-requiring) todo states. Default is ['TODO']. 829 | 830 | =head2 done_states => ARRAY 831 | 832 | List of known done (non-action-requiring) states. Default is ['DONE']. 833 | 834 | =head2 priorities => ARRAY 835 | 836 | List of known priorities. Default is ['A', 'B', 'C']. 837 | 838 | =head2 drawer_names => ARRAY 839 | 840 | List of known drawer names. Default is [qw/CLOCK LOGBOOK PROPERTIES/]. 841 | 842 | =head2 properties => ARRAY 843 | 844 | File-wide properties. 845 | 846 | =head2 radio_targets => ARRAY 847 | 848 | List of radio target text. 849 | 850 | =head2 time_zone => ARRAY 851 | 852 | If set, will be passed to DateTime->new() (e.g. by L). 853 | 854 | =head2 ignore_unknown_settings => bool 855 | 856 | If set to true, unknown settings will not cause a parse failure. 857 | 858 | 859 | =head1 METHODS 860 | 861 | =for Pod::Coverage BUILD 862 | 863 | =head2 new 864 | 865 | Usage: 866 | 867 | $doc = Org::Document->new(%args); 868 | 869 | Create document object. If C argument is specified, will parse 870 | the string. Otherwise, will create an empty document object. Arguments: 871 | 872 | =over 873 | 874 | =item * from_string 875 | 876 | String. String to parse into document object tree content. 877 | 878 | =back 879 | 880 | =head2 load_element_modules() 881 | 882 | Load all Org::Element::* modules. This is useful when wanting to work with 883 | element objects retrieved from serialization, where the element modules have not 884 | been loaded. 885 | 886 | =head2 cmp_priorities($p1, $p2) => -1|0|-1 887 | 888 | Compare two priorities C<$p1> and C<$p2>. Return result like Perl's C: 0 if 889 | the two are the same, -1 if C<$p1> is of I priority (since it's more to 890 | the left position in priority list, which is sorted highest-first) than C<$p2>, 891 | and 1 if C<$p2> is of I priority than C<$p1>. 892 | 893 | If either C<$p1> or C<$p2> has unknown priority, will return undef. 894 | 895 | Examples: 896 | 897 | $doc->cmp_priorities('A', 'A') # -> 0 898 | $doc->cmp_priorities('A', 'B') # -> -1 (A is higher than B) 899 | $doc->cmp_priorities('C', 'B') # -> 1 (C is lower than B) 900 | $doc->cmp_priorities('X', 'A') # -> undef (X is unknown) 901 | 902 | Note that X could be known if there is a C<#+PRIORITIES> setting which defines 903 | it. 904 | 905 | =cut 906 | -------------------------------------------------------------------------------- /lib/Org/Element.pm: -------------------------------------------------------------------------------- 1 | package Org::Element; 2 | 3 | use 5.010; 4 | use locale; 5 | use Log::ger; 6 | use Moo; 7 | use Scalar::Util qw(refaddr); 8 | 9 | # AUTHORITY 10 | # DATE 11 | # DIST 12 | # VERSION 13 | 14 | has document => (is => 'rw'); 15 | has parent => (is => 'rw'); 16 | has children => (is => 'rw'); 17 | 18 | # store the raw string (to preserve original formatting), not all elements use 19 | # this, usually only more complex elements 20 | has _str => (is => 'rw'); 21 | has _str_include_children => (is => 'rw'); 22 | 23 | sub die { 24 | my ($self, $msg) = @_; 25 | die $msg . 26 | " (element: ".ref($self). 27 | ", document: ".($self->document && $self->document->_srclabel ? $self->document->_srclabel : "-").")"; 28 | } 29 | 30 | sub children_as_string { 31 | my ($self) = @_; 32 | return "" unless $self->children; 33 | join "", map {$_->as_string} @{$self->children}; 34 | } 35 | 36 | sub as_string { 37 | my ($self) = @_; 38 | 39 | if (defined $self->_str) { 40 | return $self->_str . 41 | ($self->_str_include_children ? "" : $self->children_as_string); 42 | } else { 43 | return "" . $self->children_as_string; 44 | } 45 | } 46 | 47 | sub seniority { 48 | my ($self) = @_; 49 | my $c; 50 | return -4 unless $self->parent && ($c = $self->parent->children); 51 | my $addr = refaddr($self); 52 | for (my $i=0; $i < @$c; $i++) { 53 | return $i if refaddr($c->[$i]) == $addr; 54 | } 55 | return undef; ## no critic: Subroutines::ProhibitExplicitReturnUndef 56 | } 57 | 58 | sub prev_sibling { 59 | my ($self) = @_; 60 | 61 | my $sen = $self->seniority; 62 | return undef unless defined($sen) && $sen > 0; ## no critic: Subroutines::ProhibitExplicitReturnUndef 63 | my $c = $self->parent->children; 64 | $c->[$sen-1]; 65 | } 66 | 67 | sub next_sibling { 68 | my ($self) = @_; 69 | 70 | my $sen = $self->seniority; 71 | return undef unless defined($sen); ## no critic: Subroutines::ProhibitExplicitReturnUndef 72 | my $c = $self->parent->children; 73 | return undef unless $sen < @$c-1; ## no critic: Subroutines::ProhibitExplicitReturnUndef 74 | $c->[$sen+1]; 75 | } 76 | 77 | sub extra_walkables { return () } 78 | 79 | sub walk { 80 | my ($self, $code, $_level) = @_; 81 | $_level //= 0; 82 | $code->($self, $_level); 83 | if ($self->children) { 84 | # we need to copy children first to a temporary array so that in the 85 | # event when during walk a child is removed, all the children are still 86 | # walked into. 87 | my @children = @{ $self->children }; 88 | for (@children) { 89 | $_->walk($code, $_level+1); 90 | } 91 | } 92 | $_->walk($code, $_level+1) for $self->extra_walkables; 93 | } 94 | 95 | sub find { 96 | my ($self, $criteria) = @_; 97 | return unless $self->children; 98 | my @res; 99 | $self->walk( 100 | sub { 101 | my $el = shift; 102 | if (ref($criteria) eq 'CODE') { 103 | push @res, $el if $criteria->($el); 104 | } elsif ($criteria =~ /^\w+$/) { 105 | push @res, $el if $el->isa("Org::Element::$criteria"); 106 | } else { 107 | push @res, $el if $el->isa($criteria); 108 | } 109 | }); 110 | @res; 111 | } 112 | 113 | sub walk_parents { 114 | my ($self, $code) = @_; 115 | my $parent = $self->parent; 116 | while ($parent) { 117 | return $parent unless $code->($self, $parent); 118 | $parent = $parent->parent; 119 | } 120 | return; 121 | } 122 | 123 | sub headline { 124 | my ($self) = @_; 125 | my $h; 126 | $self->walk_parents( 127 | sub { 128 | my ($el, $p) = @_; 129 | if ($p->isa('Org::Element::Headline')) { 130 | $h = $p; 131 | return; 132 | } 133 | 1; 134 | }); 135 | $h; 136 | } 137 | 138 | sub headlines { 139 | my ($self) = @_; 140 | my @res; 141 | $self->walk_parents( 142 | sub { 143 | my ($el, $p) = @_; 144 | if ($p->isa('Org::Element::Headline')) { 145 | push @res, $p; 146 | } 147 | 1; 148 | }); 149 | @res; 150 | } 151 | 152 | sub settings { 153 | my ($self, $criteria) = @_; 154 | 155 | my @settings = grep { $_->isa("Org::Element::Setting") } 156 | @{ $self->children }; 157 | if ($criteria) { 158 | if (ref $criteria eq 'CODE') { 159 | @settings = grep { $criteria->($_) } @settings; 160 | } else { 161 | @settings = grep { $_->name eq $criteria } @settings; 162 | } 163 | } 164 | @settings; 165 | } 166 | 167 | sub field_name { 168 | my ($self) = @_; 169 | 170 | my $prev = $self->prev_sibling; 171 | if ($prev && $prev->isa('Org::Element::Text')) { 172 | my $text = $prev->as_string; 173 | if ($text =~ /(?:\A|\R)\s*(.+?)\s*:\s*\z/) { 174 | return $1; 175 | } 176 | } 177 | my $parent = $self->parent; 178 | if ($parent && $parent->isa('Org::Element::ListItem')) { 179 | my $list = $parent->parent; 180 | if ($list->type eq 'D') { 181 | return $parent->desc_term->as_string; 182 | } 183 | } 184 | # TODO 185 | #if ($parent && $parent->isa('Org::Element::Drawer') && 186 | # $parent->name eq 'PROPERTIES') { 187 | #} 188 | return; 189 | } 190 | 191 | sub remove { 192 | my ($self) = @_; 193 | my $parent = $self->parent; 194 | return unless $parent; 195 | splice @{$parent->children}, $self->seniority, 1; 196 | } 197 | 198 | 1; 199 | # ABSTRACT: Base class for Org document elements 200 | 201 | =head1 SYNOPSIS 202 | 203 | # Don't use directly, use the other Org::Element::* classes. 204 | 205 | 206 | =head1 DESCRIPTION 207 | 208 | This is the base class for all the other Org element classes. 209 | 210 | 211 | =head1 ATTRIBUTES 212 | 213 | =head2 document => DOCUMENT 214 | 215 | Link to document object. Elements need this to access file-wide settings, 216 | properties, etc. 217 | 218 | =head2 parent => undef | ELEMENT 219 | 220 | Link to parent element. Undef if this element is the root element. 221 | 222 | =head2 children => undef | ARRAY_OF_ELEMENTS 223 | 224 | 225 | =head1 METHODS 226 | 227 | =head2 $el->children_as_string() => STR 228 | 229 | Return a concatenation of children's as_string(), or "" if there are no 230 | children. 231 | 232 | =head2 $el->as_string() => STR 233 | 234 | Return the string representation of element. The default implementation will 235 | just use _str (if defined) concatenated with children_as_string(). 236 | 237 | =head2 $el->seniority => INT 238 | 239 | Find out the ranking of brothers/sisters of all sibling. If we are the first 240 | child of parent, return 0. If we are the second child, return 1, and so on. 241 | 242 | =head2 $el->prev_sibling() => ELEMENT | undef 243 | 244 | =head2 $el->next_sibling() => ELEMENT | undef 245 | 246 | =head2 $el->extra_walkables => LIST 247 | 248 | Return extra walkable elements. The default is to return an empty list, but some 249 | elements can have this, for L's title is also a walkable 250 | element. 251 | 252 | =head2 $el->walk(CODEREF) 253 | 254 | Call CODEREF for node and all descendent nodes (and extra walkables), 255 | depth-first. Code will be given the element object as argument. 256 | 257 | =head2 $el->find(CRITERIA) => ELEMENTS 258 | 259 | Find subelements. CRITERIA can be a word (e.g. 'Headline' meaning of class 260 | 'Org::Element::Headline') or a class name ('Org::Element::ListItem') or a 261 | coderef (which will be given the element to test). Will return matched elements. 262 | 263 | =head2 $el->walk_parents(CODE) 264 | 265 | Run CODEREF for parent, and its parent, and so on until the root element (the 266 | document), or until CODEREF returns a false value. CODEREF will be supplied 267 | ($el, $parent). Will return the last parent walked. 268 | 269 | =head2 $el->headline() => ELEMENT 270 | 271 | Get current headline. Return undef if element is not under any headline. 272 | 273 | =head2 $el->headlines() => ELEMENTS 274 | 275 | Get current headline (in the first element of the result list), its parent, its 276 | parent's parent, and so on until the topmost headline. Return empty list if 277 | element is not under any headline. 278 | 279 | =head2 $el->settings(CRITERIA) => ELEMENTS 280 | 281 | Get L nodes directly under the element. Equivalent to: 282 | 283 | my @settings = grep { $_->isa("Org::Element::Setting") } @{ $el->children }; 284 | 285 | If CRITERIA is specified, will filter based on some criteria. CRITERIA can be a 286 | coderef, or a string to filter by setting's name, example: 287 | 288 | my ($doc_title) = $doc->settings('TITLE'); 289 | 290 | Take note of the list operator on the left because C return a list. 291 | 292 | =head2 $el->field_name() => STR 293 | 294 | Try to extract "field name", being defined as either some text on the left side: 295 | 296 | DEADLINE: <2011-06-09 > 297 | 298 | or a description term in a description list: 299 | 300 | - wedding anniversary :: <2011-06-10 > 301 | 302 | =head2 $el->remove() 303 | 304 | Remove element from the tree. Basically just remove the element from its parent. 305 | 306 | =head2 $el->die(STR) 307 | 308 | Utility method to format C message. 309 | 310 | =cut 311 | -------------------------------------------------------------------------------- /lib/Org/Element/Block.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Block; 2 | 3 | use 5.010; 4 | use locale; 5 | 6 | use Moo; 7 | extends 'Org::Element'; 8 | with 'Org::ElementRole'; 9 | with 'Org::ElementRole::Block'; 10 | 11 | # AUTHORITY 12 | # DATE 13 | # DIST 14 | # VERSION 15 | 16 | has name => (is => 'rw'); 17 | has args => (is => 'rw'); 18 | has raw_content => (is => 'rw'); 19 | has begin_indent => (is => 'rw'); 20 | has end_indent => (is => 'rw'); 21 | 22 | my @known_blocks = qw( 23 | ASCII CENTER COMMENT EXAMPLE HTML 24 | LATEX QUOTE SRC VERSE 25 | ); 26 | 27 | sub BUILD { 28 | my ($self, $args) = @_; 29 | $self->name(uc $self->name); 30 | (grep { $_ eq $self->name } @known_blocks) 31 | or $self->die("Unknown block name: ".$self->name); 32 | } 33 | 34 | sub element_as_string { 35 | my ($self) = @_; 36 | return $self->_str if defined $self->_str; 37 | join("", 38 | $self->begin_indent // "", 39 | "#+BEGIN_".uc($self->name), 40 | $self->args && @{$self->args} ? 41 | " ".Org::Document::__format_args($self->args) : "", 42 | "\n", 43 | $self->raw_content, 44 | $self->end_indent // "", 45 | "#+END_".uc($self->name)."\n"); 46 | } 47 | 48 | 1; 49 | # ABSTRACT: Represent Org block 50 | 51 | =for Pod::Coverage element_as_string BUILD 52 | 53 | =head1 DESCRIPTION 54 | 55 | Derived from L. 56 | 57 | 58 | =head1 ATTRIBUTES 59 | 60 | =head2 name => STR 61 | 62 | Block name. For example, #+begin_src ... #+end_src is an 'SRC' block. 63 | 64 | =head2 args => ARRAY 65 | 66 | =head2 raw_content => STR 67 | 68 | =head2 begin_indent => STR 69 | 70 | Indentation on begin line (before C<#+BEGIN>), or empty string if none. 71 | 72 | =head2 end_indent => STR 73 | 74 | Indentation on end line (before C<#+END>), or empty string if none. 75 | 76 | 77 | =head1 METHODS 78 | 79 | =cut 80 | -------------------------------------------------------------------------------- /lib/Org/Element/Comment.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Comment; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | with 'Org::ElementRole'; 8 | with 'Org::ElementRole::Block'; 9 | 10 | # AUTHORITY 11 | # DATE 12 | # DIST 13 | # VERSION 14 | 15 | 1; 16 | # ABSTRACT: Represent Org comment 17 | 18 | =head1 DESCRIPTION 19 | 20 | Derived from L. 21 | 22 | 23 | =head1 ATTRIBUTES 24 | 25 | 26 | =head1 METHODS 27 | 28 | =cut 29 | -------------------------------------------------------------------------------- /lib/Org/Element/Drawer.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Drawer; 2 | 3 | use 5.010; 4 | use locale; 5 | 6 | use Moo; 7 | extends 'Org::Element'; 8 | with 'Org::ElementRole'; 9 | with 'Org::ElementRole::Block'; 10 | 11 | # AUTHORITY 12 | # DATE 13 | # DIST 14 | # VERSION 15 | 16 | has name => (is => 'rw'); 17 | has properties => (is => 'rw'); 18 | 19 | sub BUILD { 20 | my ($self, $args) = @_; 21 | my $doc = $self->document; 22 | my $pass = $args->{pass} // 1; 23 | 24 | if ($pass == 2) { 25 | $self->die("Unknown drawer name: ".$self->name) 26 | unless grep { $_ eq $self->name } @{$doc->drawer_names}; 27 | } 28 | } 29 | 30 | sub _parse_properties { 31 | my ($self, $raw_content) = @_; 32 | $self->properties({}) unless $self->properties; 33 | while ($raw_content =~ /^[ \t]*:(\w+):[ \t]+ 34 | ($Org::Document::args_re)[ \t]*(?:\R|\z)/mxg) { 35 | $self->properties->{$1} = $2; 36 | } 37 | } 38 | 39 | sub as_string { 40 | my ($self) = @_; 41 | join("", 42 | ":", $self->name, ":\n", 43 | $self->children_as_string, 44 | ":END:"); 45 | } 46 | 47 | 1; 48 | # ABSTRACT: Represent Org drawer 49 | 50 | =for Pod::Coverage BUILD as_string 51 | 52 | =head1 DESCRIPTION 53 | 54 | Derived from L. 55 | 56 | Example of a drawer in an Org document: 57 | 58 | * A heading 59 | :SOMEDRAWER: 60 | some text 61 | more text ... 62 | :END: 63 | 64 | A special drawer named C is used to store a list of properties: 65 | 66 | * A heading 67 | :PROPERTIES: 68 | :Title: the title 69 | :Publisher: the publisher 70 | :END: 71 | 72 | 73 | =head1 ATTRIBUTES 74 | 75 | =head2 name => STR 76 | 77 | Drawer name. 78 | 79 | =head2 properties => HASH 80 | 81 | Collected properties in the drawer. In the example properties drawer above, 82 | C will result in: 83 | 84 | { 85 | Title => "the title", 86 | Publisher => "the publisher", 87 | } 88 | 89 | 90 | =head1 METHODS 91 | 92 | =cut 93 | -------------------------------------------------------------------------------- /lib/Org/Element/FixedWidthSection.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::FixedWidthSection; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | with 'Org::ElementRole'; 8 | with 'Org::ElementRole::Block'; 9 | 10 | # AUTHORITY 11 | # DATE 12 | # DIST 13 | # VERSION 14 | 15 | sub text { 16 | my ($self) = @_; 17 | my $res = $self->_str; 18 | $res =~ s/^[ \t]*: ?//mg; 19 | $res; 20 | } 21 | 22 | 1; 23 | # ABSTRACT: Represent Org fixed-width section 24 | 25 | =head1 SYNOPSIS 26 | 27 | use Org::Element::FixedWidthSection; 28 | my $el = Org::Element::FixedWidthSection->new(_str => ": line1\n: line2\n"); 29 | 30 | 31 | =head1 DESCRIPTION 32 | 33 | Fixed width section is a block of text where each line is prefixed by colon + 34 | space (or just a colon + space or a colon). Example: 35 | 36 | Here is an example: 37 | : some example from a text file. 38 | : second line. 39 | : 40 | : fourth line, after the empty above. 41 | 42 | which is functionally equivalent to: 43 | 44 | Here is an example: 45 | #+BEGIN_EXAMPLE 46 | some example from a text file. 47 | another example. 48 | 49 | fourth line, after the empty above. 50 | #+END_EXAMPLE 51 | 52 | Derived from L. 53 | 54 | 55 | =head1 ATTRIBUTES 56 | 57 | 58 | =head1 METHODS 59 | 60 | =head2 $el->text => STR 61 | 62 | The text (without colon prefix). 63 | 64 | 65 | =for Pod::Coverage as_string BUILD 66 | 67 | =cut 68 | -------------------------------------------------------------------------------- /lib/Org/Element/Footnote.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Footnote; 2 | 3 | use 5.010; 4 | use locale; 5 | use Log::ger; 6 | use Moo; 7 | extends 'Org::Element'; 8 | with 'Org::ElementRole'; 9 | with 'Org::ElementRole::Inline'; 10 | 11 | # AUTHORITY 12 | # DATE 13 | # DIST 14 | # VERSION 15 | 16 | has name => (is => 'rw'); 17 | has is_ref => (is => 'rw'); 18 | has def => (is => 'rw'); 19 | 20 | sub BUILD { 21 | my ($self, $args) = @_; 22 | log_trace("name = %s", $self->name); 23 | } 24 | 25 | sub as_string { 26 | my ($self) = @_; 27 | 28 | join("", 29 | "[fn:", ($self->name // ""), 30 | defined($self->def) ? ":".$self->def->as_string : "", 31 | "]"); 32 | } 33 | 34 | sub as_text { 35 | goto \&as_string; 36 | } 37 | 38 | 1; 39 | # ABSTRACT: Represent Org footnote reference and/or definition 40 | 41 | =for Pod::Coverage ^(BUILD)$ 42 | 43 | =head1 DESCRIPTION 44 | 45 | Derived from L. 46 | 47 | 48 | =head1 ATTRIBUTES 49 | 50 | =head2 name => STR|undef 51 | 52 | Can be undef, for anonymous footnote (but in case of undef, is_ref must be 53 | true and def must also be set). 54 | 55 | =head2 is_ref => BOOL 56 | 57 | Set to true to make this a footnote reference. 58 | 59 | =head2 def => TEXT ELEMENT 60 | 61 | Set to make this a footnote definition. 62 | 63 | 64 | =head1 METHODS 65 | 66 | =head2 as_string => str 67 | 68 | From L. 69 | 70 | =head2 as_text => str 71 | 72 | From L. 73 | -------------------------------------------------------------------------------- /lib/Org/Element/Headline.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Headline; 2 | 3 | use 5.010; 4 | use locale; 5 | use Log::ger; 6 | 7 | use Moo; 8 | extends 'Org::Element'; 9 | with 'Org::ElementRole'; 10 | with 'Org::ElementRole::Block'; 11 | 12 | # AUTHORITY 13 | # DATE 14 | # DIST 15 | # VERSION 16 | 17 | has level => (is => 'rw'); 18 | has title => (is => 'rw'); 19 | has priority => (is => 'rw'); 20 | has tags => (is => 'rw'); 21 | has is_todo => (is => 'rw'); 22 | has is_done => (is => 'rw'); 23 | has todo_state => (is => 'rw'); 24 | has statistics_cookie => (is => 'rw'); 25 | 26 | # old name, deprecated since 2014-07-17, will be removed in the future 27 | sub todo_priority { shift->priority(@_) } 28 | 29 | sub extra_walkables { 30 | my $self = shift; 31 | grep {defined} ($self->title); 32 | } 33 | 34 | sub header_as_string { 35 | my ($self) = @_; 36 | return $self->_str if defined $self->_str; 37 | join("", 38 | "*" x $self->level, 39 | " ", 40 | $self->is_todo ? $self->todo_state." " : "", 41 | $self->priority ? "[#".$self->priority."] " : "", 42 | $self->statistics_cookie ? "[".$self->statistics_cookie."] " : "", 43 | $self->title->as_string, 44 | $self->tags && @{$self->tags} ? 45 | " :".join(":", @{$self->tags}).":" : "", 46 | "\n"); 47 | } 48 | 49 | sub as_string { 50 | my ($self) = @_; 51 | $self->header_as_string . $self->children_as_string; 52 | } 53 | 54 | sub get_tags { 55 | my ($self, $name) = @_; 56 | my @res = @{ $self->tags // [] }; 57 | $self->walk_parents( 58 | sub { 59 | my ($el, $parent) = @_; 60 | return 1 unless $parent->isa('Org::Element::Headline'); 61 | if ($parent->tags) { 62 | for my $tag (@{ $parent->tags }) { 63 | push @res, $tag unless grep { $_ eq $tag } @res; 64 | } 65 | } 66 | 1; 67 | }); 68 | for my $tag (@{ $self->document->tags }) { 69 | push @res, $tag unless grep { $_ eq $tag } @res; 70 | } 71 | @res; 72 | } 73 | 74 | sub get_active_timestamp { 75 | my ($self) = @_; 76 | 77 | for my $s ($self->title, $self) { 78 | my $ats; 79 | $s->walk( 80 | sub { 81 | my ($el) = @_; 82 | return if $ats; 83 | $ats = $el if $el->isa('Org::Element::Timestamp') && 84 | $el->is_active; 85 | } 86 | ); 87 | return $ats if $ats; 88 | } 89 | return; 90 | } 91 | 92 | sub is_leaf { 93 | my ($self) = @_; 94 | 95 | return 1 unless $self->children; 96 | 97 | my $res; 98 | for my $child (@{ $self->children }) { 99 | $child->walk( 100 | sub { 101 | return if defined($res); 102 | my ($el) = @_; 103 | if ($el->isa('Org::Element::Headline')) { 104 | $res = 0; 105 | goto EXIT_WALK; 106 | } 107 | } 108 | ); 109 | } 110 | EXIT_WALK: 111 | $res //= 1; 112 | $res; 113 | } 114 | 115 | sub promote_node { 116 | my ($self, $num_levels) = @_; 117 | $num_levels //= 1; 118 | return if $num_levels == 0; 119 | $self->die("Please specify a positive number of levels") if $num_levels < 0; 120 | 121 | for my $i (1..$num_levels) { 122 | 123 | my $l = $self->level; 124 | last if $l <= 1; 125 | $l--; 126 | $self->level($l); 127 | 128 | $self->_str(undef); 129 | 130 | my $parent = $self->parent; 131 | my $siblings = $parent->children; 132 | my $pos = $self->seniority; 133 | 134 | # our children stay as children 135 | 136 | # our right sibling headline(s) become children 137 | while (1) { 138 | my $s = $siblings->[$pos+1]; 139 | last unless $s && $s->isa('Org::Element::Headline') 140 | && $s->level > $l; 141 | $self->children([]) unless defined $self->children; 142 | push @{$self->children}, $s; 143 | splice @$siblings, $pos+1, 1; 144 | $s->parent($self); 145 | } 146 | 147 | # our parent headline can become sibling if level is the same 148 | if ($parent->isa('Org::Element::Headline') && $parent->level == $l) { 149 | splice @$siblings, $pos, 1; 150 | my $gparent = $parent->parent; 151 | splice @{$gparent->children}, $parent->seniority+1, 0, $self; 152 | $self->parent($gparent); 153 | } 154 | 155 | } 156 | } 157 | 158 | sub demote_node { 159 | my ($self, $num_levels) = @_; 160 | $num_levels //= 1; 161 | return if $num_levels == 0; 162 | $self->die("Please specify a positive number of levels") if $num_levels < 0; 163 | 164 | for my $i (1..$num_levels) { 165 | 166 | my $l = $self->level; 167 | $l++; 168 | $self->level($l); 169 | 170 | $self->_str(undef); 171 | 172 | # prev sibling can become parent 173 | my $ps = $self->prev_sibling; 174 | if ($ps && $ps->isa('Org::Element::Headline') && $ps->level < $l) { 175 | splice @{$self->parent->children}, $self->seniority, 1; 176 | $ps->children([]) if !defined($ps->children); 177 | push @{$ps->children}, $self; 178 | $self->parent($ps); 179 | } 180 | 181 | } 182 | } 183 | 184 | sub promote_branch { 185 | my ($self, $num_levels) = @_; 186 | $num_levels //= 1; 187 | return if $num_levels == 0; 188 | $self->die("Please specify a positive number of levels") if $num_levels < 0; 189 | 190 | for my $i (1..$num_levels) { 191 | last if $self->level <= 1; 192 | $_->promote_node() for $self->find('Headline'); 193 | } 194 | } 195 | 196 | sub demote_branch { 197 | my ($self, $num_levels) = @_; 198 | $num_levels //= 1; 199 | return if $num_levels == 0; 200 | $self->die("Please specify a positive number of levels") if $num_levels < 0; 201 | 202 | for my $i (1..$num_levels) { 203 | $_->demote_node() for $self->find('Headline'); 204 | } 205 | } 206 | 207 | sub get_drawer { 208 | my $self = shift; 209 | my $wanted_drawer_name = shift || "PROPERTIES"; 210 | 211 | for my $d (@{$self->children||[]}) { 212 | log_trace("seeking $wanted_drawer_name drawer in child: %s (%s)", $d->as_string, ref($d)); 213 | next unless ($d->isa('Org::Element::Drawer') 214 | && $d->name eq $wanted_drawer_name 215 | && $d->properties); 216 | return $d; 217 | } 218 | } 219 | 220 | sub get_property { 221 | my ($self, $name, $search_parent, $search_docprop) = @_; 222 | #$log->tracef("-> get_property(%s, search_par=%s)", $name, $search_parent); 223 | my $parent = $self->parent; 224 | 225 | my $propd = $self->get_drawer("PROPERTIES"); 226 | return $propd->properties->{$name} if 227 | $propd && defined $propd->properties->{$name}; 228 | 229 | if ($parent && $search_parent) { 230 | while ($parent) { 231 | if ($parent->isa('Org::Element::Headline')) { 232 | my $res = $parent->get_property($name, 0, 0); 233 | return $res if defined $res; 234 | } 235 | $parent = $parent->parent; 236 | } 237 | } 238 | 239 | if ($search_docprop // 1) { 240 | log_trace("Getting property from document's .properties"); 241 | return $self->document->properties->{$name}; 242 | } 243 | undef; 244 | } 245 | 246 | sub update_statistics_cookie { 247 | my $self = shift; 248 | 249 | my $statc = $self->statistics_cookie; 250 | return unless $statc; 251 | 252 | my $num_done = 0; 253 | my $num_total = 0; 254 | 255 | # count using checks on first-level list's children, or from first-level 256 | # subheadlines 257 | for my $chld (@{ $self->children // [] }) { 258 | if ($chld->isa("Org::Element::Headline")) { 259 | for my $el (@{ $self->children }) { 260 | next unless $el->isa("Org::Element::Headline"); 261 | if ($el->is_todo) { 262 | $num_total++; 263 | $num_done++ if $el->is_done; 264 | } 265 | } 266 | last; 267 | } elsif ($chld->isa("Org::Element::List")) { 268 | for my $el (@{ $self->children }) { 269 | next unless $el->isa("Org::Element::List"); 270 | for my $el2 (@{ $el->children }) { 271 | next unless $el2->isa("Org::Element::ListItem"); 272 | my $state = $el2->check_state; 273 | if (defined $state) { 274 | $num_total++; 275 | $num_done++ if $state eq 'X'; 276 | } 277 | } 278 | } 279 | last; 280 | } 281 | } 282 | 283 | undef $self->{_str}; # we modify content 284 | if ($statc =~ /%/) { 285 | $self->statistics_cookie( 286 | sprintf("%d%%", $num_total == 0 ? 0 : $num_done/$num_total * 100)); 287 | } else { 288 | $self->statistics_cookie(sprintf("%d/%d", $num_done, $num_total)); 289 | } 290 | } 291 | 292 | 1; 293 | # ABSTRACT: Represent Org headline 294 | 295 | =for Pod::Coverage ^(header_as_string|as_string|todo_priority)$ 296 | 297 | =head1 DESCRIPTION 298 | 299 | Derived from L. 300 | 301 | 302 | =head1 ATTRIBUTES 303 | 304 | =head2 level => INT 305 | 306 | Level of headline (e.g. 1, 2, 3). Corresponds to the number of bullet stars. 307 | 308 | =head2 title => OBJ 309 | 310 | L representing the headline title 311 | 312 | =head2 priority => STR 313 | 314 | String (optional) representing priority. 315 | 316 | =head2 tags => ARRAY 317 | 318 | Arrayref (optional) containing list of defined tags. 319 | 320 | =head2 is_todo => BOOL 321 | 322 | Whether this headline is a TODO item. 323 | 324 | =head2 is_done => BOOL 325 | 326 | Whether this TODO item is in a done state (state which requires no more action, 327 | e.g. DONE). Only meaningful if headline is a TODO item. 328 | 329 | =head2 todo_state => STR 330 | 331 | TODO state. 332 | 333 | =head2 statistics_cookie => STR 334 | 335 | Statistics cookie, e.g. '5/10' or '50%'. TODO: there might be more than one 336 | statistics cookie. 337 | 338 | 339 | =head1 METHODS 340 | 341 | =head2 $el->get_tags() => ARRAY 342 | 343 | Get tags for this headline. A headline can define tags or inherit tags from its 344 | parent headline (or from document). 345 | 346 | =head2 $el->get_active_timestamp() => ELEMENT 347 | 348 | Get the first active timestamp element for this headline, either in the title or 349 | in the child elements. 350 | 351 | =head2 $el->is_leaf() => BOOL 352 | 353 | Returns true if element doesn't contain subtrees. 354 | 355 | =head2 $el->promote_node([$num_levels]) 356 | 357 | Promote (decrease the level) of this headline node. $level specifies number of 358 | levels, defaults to 1. Won't further promote if already at level 1. 359 | Illustration: 360 | 361 | * h1 362 | ** h2 <-- promote 1 level 363 | *** h3 364 | *** h3b 365 | ** h4 366 | * h5 367 | 368 | becomes: 369 | 370 | * h1 371 | * h2 372 | *** h3 373 | *** h3b 374 | ** h4 375 | * h5 376 | 377 | =head2 $el->demote_node([$num_levels]) 378 | 379 | Does the opposite of promote_node(). 380 | 381 | =head2 $el->promote_branch([$num_levels]) 382 | 383 | Like promote_node(), but all children headlines will also be promoted. 384 | Illustration: 385 | 386 | * h1 387 | ** h2 <-- promote 1 level 388 | *** h3 389 | **** grandkid 390 | *** h3b 391 | 392 | ** h4 393 | * h5 394 | 395 | becomes: 396 | 397 | * h1 398 | * h2 399 | ** h3 400 | *** grandkid 401 | ** h3b 402 | 403 | ** h4 404 | * h5 405 | 406 | =head2 $el->demote_branch([$num_levels]) 407 | 408 | Does the opposite of promote_branch(). 409 | 410 | =head2 $el->get_property($name, $search_parent) => VALUE 411 | 412 | Search for property named $name in the PROPERTIES drawer. If $search_parent is 413 | set to true (default is false), will also search in upper-level properties 414 | (useful for searching for inherited property, like foo_ALL). Return undef if 415 | property cannot be found. 416 | 417 | Regardless of $search_parent setting, file-wide properties will be consulted if 418 | property is not found in the headline's properties drawer. 419 | 420 | =head2 $el->get_drawer([$drawer_name]) => VALUE 421 | 422 | Return an entire drawer as an Org::Element::Drawer object. By default, return the 423 | PROPERTIES drawer. If you want LOGBOOK or some other drawer, ask for it by name. 424 | 425 | =head2 $el->update_statistics_cookie 426 | 427 | Update the statistics cookies by recalculating the number of TODO and 428 | checkboxes. 429 | 430 | Will do nothing if the headline does not have any statistics cookie. 431 | 432 | =cut 433 | -------------------------------------------------------------------------------- /lib/Org/Element/Link.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Link; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | with 'Org::ElementRole'; 8 | with 'Org::ElementRole::Inline'; 9 | 10 | # AUTHORITY 11 | # DATE 12 | # DIST 13 | # VERSION 14 | 15 | has link => (is => 'rw'); 16 | has description => (is => 'rw'); 17 | has from_radio_target => (is => 'rw'); 18 | 19 | sub as_string { 20 | my ($self) = @_; 21 | return $self->_str if defined $self->_str; 22 | join("", 23 | "[", 24 | "[", $self->link, "]", 25 | (defined($self->description) && length($self->description) ? 26 | ("[", $self->description->as_string, "]") : ()), 27 | "]"); 28 | } 29 | 30 | sub as_text { 31 | my $self = shift; 32 | my $desc = $self->description; 33 | defined($desc) ? $desc->as_text : $self->link; 34 | } 35 | 36 | 1; 37 | # ABSTRACT: Represent Org hyperlink 38 | 39 | =head1 DESCRIPTION 40 | 41 | Derived from L. 42 | 43 | 44 | =head1 ATTRIBUTES 45 | 46 | =head2 link => STR 47 | 48 | =head2 description => OBJ 49 | 50 | =head2 from_radio_target => BOOL 51 | 52 | 53 | =head1 METHODS 54 | 55 | =head2 as_string => str 56 | 57 | From L. 58 | 59 | =head2 as_text => str 60 | 61 | From L. 62 | -------------------------------------------------------------------------------- /lib/Org/Element/List.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::List; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | 8 | # AUTHORITY 9 | # DATE 10 | # DIST 11 | # VERSION 12 | 13 | has indent => (is => 'rw'); 14 | has type => (is => 'rw'); 15 | has bullet_style => (is => 'rw'); 16 | 17 | sub items { 18 | my $self = shift; 19 | my @items; 20 | for (@{ $self->children }) { 21 | push @items, $_ if $_->isa('Org::Element::ListItem'); 22 | } 23 | \@items; 24 | } 25 | 26 | 1; 27 | # ABSTRACT: Represent Org list 28 | 29 | =for Pod::Coverage 30 | 31 | =head1 DESCRIPTION 32 | 33 | Must have L (or another ::List) as children. 34 | 35 | Derived from L. 36 | 37 | 38 | =head1 ATTRIBUTES 39 | 40 | =head2 indent 41 | 42 | Indent (e.g. " " x 2). 43 | 44 | =head2 type 45 | 46 | 'U' for unordered list (-, +, * for bullets), 'D' for description list, 'O' for 47 | ordered list (1., 2., 3., and so on). 48 | 49 | =head2 bullet_style 50 | 51 | E.g. '-', '*', '+'. For ordered list, currently just use '.' 52 | 53 | 54 | =head1 METHODS 55 | 56 | =head2 $list->items() => ARRAY OF OBJECTS 57 | 58 | Return the items, which are an arrayref of L objects. 59 | 60 | =cut 61 | -------------------------------------------------------------------------------- /lib/Org/Element/ListItem.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::ListItem; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | 8 | # AUTHORITY 9 | # DATE 10 | # DIST 11 | # VERSION 12 | 13 | has bullet => (is => 'rw'); 14 | has check_state => (is => 'rw'); 15 | has desc_term => (is => 'rw'); 16 | 17 | sub header_as_string { 18 | my ($self) = @_; 19 | join("", 20 | $self->parent->indent, 21 | $self->bullet, " ", 22 | defined($self->check_state) ? "[".$self->check_state."]" : "", 23 | defined($self->desc_term) ? $self->desc_term->as_string . " ::" : "", 24 | ); 25 | } 26 | 27 | sub as_string { 28 | my ($self) = @_; 29 | $self->header_as_string . $self->children_as_string; 30 | } 31 | 32 | 1; 33 | #ABSTRACT: Represent Org list item 34 | 35 | =head1 DESCRIPTION 36 | 37 | Must have L as parent. 38 | 39 | Derived from L. 40 | 41 | 42 | =head1 ATTRIBUTES 43 | 44 | =head2 bullet 45 | 46 | =head2 check_state 47 | 48 | undef, " ", "X" or "-". 49 | 50 | =head2 desc_term 51 | 52 | Description term (for description list). 53 | 54 | 55 | =head1 METHODS 56 | 57 | =for Pod::Coverage header_as_string as_string 58 | 59 | =cut 60 | -------------------------------------------------------------------------------- /lib/Org/Element/RadioTarget.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::RadioTarget; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | with 'Org::ElementRole'; 8 | with 'Org::ElementRole::Inline'; 9 | 10 | # AUTHORITY 11 | # DATE 12 | # DIST 13 | # VERSION 14 | 15 | has target => (is => 'rw'); 16 | 17 | sub BUILD { 18 | my ($self, $args) = @_; 19 | my $pass = $args->{pass} // 1; 20 | my $doc = $self->document; 21 | if ($pass == 1) { 22 | push @{ $doc->radio_targets }, 23 | $self->target; 24 | } 25 | } 26 | 27 | sub as_string { 28 | my ($self) = @_; 29 | join("", 30 | "<<<", $self->target, ">>>"); 31 | } 32 | 33 | sub as_text { 34 | goto \&as_string; 35 | } 36 | 37 | 1; 38 | # ABSTRACT: Represent Org radio target 39 | 40 | =for Pod::Coverage ^(BUILD)$ 41 | 42 | =head1 DESCRIPTION 43 | 44 | Derived from L. 45 | 46 | 47 | =head1 ATTRIBUTES 48 | 49 | =head2 target 50 | 51 | 52 | =head1 METHODS 53 | 54 | =head2 as_string => str 55 | 56 | From L. 57 | 58 | =head2 as_text => str 59 | 60 | From L. 61 | 62 | =cut 63 | -------------------------------------------------------------------------------- /lib/Org/Element/Setting.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Setting; 2 | 3 | use 5.010001; 4 | use locale; 5 | 6 | use Moo; 7 | extends 'Org::Element'; 8 | with 'Org::ElementRole'; 9 | with 'Org::ElementRole::Block'; 10 | 11 | # AUTHORITY 12 | # DATE 13 | # DIST 14 | # VERSION 15 | 16 | our @known_settings = qw( 17 | ARCHIVE 18 | ASCII 19 | ATTR_ASCII 20 | ATTR_BEAMER 21 | ATTR_HTML 22 | ATTR_LATEX 23 | ATTR_ODT 24 | AUTHOR 25 | BABEL 26 | BEAMER 27 | BEAMER_COLOR_THEME 28 | BEAMER_FONT_THEME 29 | BEAMER_INNER_THEME 30 | BEAMER_OUTER_THEME 31 | BEAMER_THEME 32 | BEGIN 33 | BEGIN_ASCII 34 | BEGIN_BEAMER 35 | BEGIN_CENTER 36 | BEGIN_COMMENT 37 | BEGIN_EXAMPLE 38 | BEGIN_HTML 39 | BEGIN_LATEX 40 | BEGIN_QUOTE 41 | BEGIN_SRC 42 | BEGIN_SRC 43 | BEGIN_VERSE 44 | BIND 45 | CALL 46 | CAPTION 47 | CATEGORY 48 | COLUMNS 49 | CONSTANTS 50 | DATE 51 | DESCRIPTION 52 | DRAWERS 53 | EMAIL 54 | EXPORT_EXCLUDE_TAGS 55 | EXPORT_INCLUDE_TAGS 56 | FILETAGS 57 | HEADER 58 | HEADERS 59 | HTML 60 | HTML_HEAD 61 | HTML_HEAD_EXTRA 62 | HTML_INCLUDE_STYLE 63 | INCLUDE 64 | INDEX 65 | INFOJS_OPT 66 | KEYWORDS 67 | LABEL 68 | LANGUAGE 69 | LAST_MOBILE_CHANGE 70 | LATEX 71 | LATEX_CLASS 72 | LATEX_CLASS_OPTIONS 73 | LATEX_HEADER 74 | LATEX_HEADER_EXTRA 75 | LINK 76 | LINK_HOME 77 | LINK_UP 78 | MACRO 79 | NAME 80 | ODT_STYLES_FILE 81 | OPTIONS 82 | ORGLST 83 | ORGTBL 84 | PLOT 85 | POSTID 86 | PRIORITIES 87 | PROPERTY 88 | RESULTS 89 | SEQ_TODO 90 | SETUPFILE 91 | SRCNAME 92 | STARTUP 93 | STYLE 94 | TAGS 95 | TBLFM 96 | TEXT 97 | TITLE 98 | TOC 99 | TODO 100 | TYP_TODO 101 | XSLT 102 | ); 103 | 104 | has name => (is => 'rw'); 105 | has raw_arg => (is => 'ro'); 106 | has args => (is => 'rw'); 107 | has indent => (is => 'rw'); 108 | 109 | # static method 110 | sub indentable_settings { 111 | state $data = [qw/TBLFM/]; 112 | $data; 113 | } 114 | 115 | sub BUILD { 116 | require Org::Document; 117 | my ($self, $build_args) = @_; 118 | my $doc = $self->document; 119 | my $pass = $build_args->{pass} // 1; 120 | 121 | my $name = uc $self->name; 122 | $self->name($name); 123 | 124 | my $args = $self->args; 125 | if ($name eq 'DRAWERS') { 126 | if ($pass == 1) { 127 | for my $arg (@$args) { 128 | push @{ $doc->drawer_names }, $arg 129 | unless grep { $_ eq $arg } @{ $doc->drawer_names }; 130 | } 131 | } 132 | } elsif ($name eq 'FILETAGS') { 133 | if ($pass == 1) { 134 | no warnings 'once'; 135 | $args->[0] =~ /^$Org::Document::tags_re$/ or 136 | $self->die("Invalid argument for FILETAGS: $args->[0]"); 137 | for my $tag (split /:/, $args->[0]) { 138 | next unless length $tag; 139 | push @{ $doc->tags }, $tag 140 | unless grep { $_ eq $tag } @{ $doc->tags }; 141 | } 142 | } 143 | } elsif ($name eq 'PRIORITIES') { 144 | if ($pass == 1) { 145 | for (@$args) { 146 | push @{ $doc->priorities }, $_; 147 | } 148 | } 149 | } elsif ($name eq 'PROPERTY') { 150 | if ($pass == 1) { 151 | @$args >= 2 or $self->die("Not enough argument for PROPERTY, minimum 2"); 152 | my $name = shift @$args; 153 | $doc->properties->{$name} = @$args > 1 ? [@$args] : $args->[0]; 154 | } 155 | } elsif ($name =~ /^(SEQ_TODO|TODO|TYP_TODO)$/) { 156 | if ($pass == 1) { 157 | my $done; 158 | for (my $i=0; $i<@$args; $i++) { 159 | my $arg = $args->[$i]; 160 | if ($arg eq '|') { $done++; next } 161 | $done++ if !$done && @$args > 1 && $i == @$args-1; 162 | my $ary = $done ? $doc->done_states : $doc->todo_states; 163 | push @$ary, $arg unless grep { $_ eq $arg } @$ary; 164 | } 165 | } 166 | } else { 167 | unless ($self->document->ignore_unknown_settings) { 168 | $self->die("Unknown setting $name") unless grep { $_ eq $name } @known_settings; 169 | } 170 | } 171 | } 172 | 173 | sub as_string { 174 | my ($self) = @_; 175 | join("", 176 | $self->indent // "", 177 | "#+".uc($self->name), ":", 178 | $self->args && @{$self->args} ? 179 | " ".Org::Document::__format_args($self->args) : "", 180 | "\n" 181 | ); 182 | } 183 | 184 | 1; 185 | # ABSTRACT: Represent Org in-buffer settings 186 | 187 | =for Pod::Coverage as_string BUILD 188 | 189 | =head1 DESCRIPTION 190 | 191 | Derived from L. 192 | 193 | 194 | =head1 ATTRIBUTES 195 | 196 | =head2 name => STR 197 | 198 | Setting name. 199 | 200 | =head2 raw_arg => ARRAY 201 | 202 | String, read-only (can only be set during instantiation). Setting's raw 203 | arguments. 204 | 205 | =head2 args => ARRAY 206 | 207 | Setting's arguments. 208 | 209 | =head2 indent => STR 210 | 211 | Indentation (whitespaces before C<#+>), or empty string if none. 212 | 213 | 214 | =head1 METHODS 215 | 216 | =head2 Org::Element::Setting->indentable_settings -> ARRAY 217 | 218 | Return an arrayref containing the setting names that can be indented. In Org, 219 | some settings can be indented and some can't. Setting names are all in 220 | uppercase. 221 | 222 | =cut 223 | -------------------------------------------------------------------------------- /lib/Org/Element/Table.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Table; 2 | 3 | use 5.010; 4 | use locale; 5 | use Log::ger; 6 | use Moo; 7 | extends 'Org::Element'; 8 | with 'Org::ElementRole'; 9 | with 'Org::ElementRole::Block'; 10 | 11 | # AUTHORITY 12 | # DATE 13 | # DIST 14 | # VERSION 15 | 16 | has _dummy => (is => 'rw'); # workaround Moo bug 17 | 18 | sub BUILD { 19 | require Org::Element::TableRow; 20 | require Org::Element::TableHLine; 21 | require Org::Element::TableCell; 22 | my ($self, $args) = @_; 23 | my $pass = $args->{pass} // 1; 24 | 25 | # parse _str into rows & cells 26 | my $_str = $args->{_str}; 27 | if (defined $_str && !defined($self->children)) { 28 | 29 | if (!defined($self->_str_include_children)) { 30 | $self->_str_include_children(1); 31 | } 32 | 33 | my $doc = $self->document; 34 | my @rows0 = split /\R/, $_str; 35 | $self->children([]); 36 | for my $row0 (@rows0) { 37 | log_trace("table line: %s", $row0); 38 | next unless $row0 =~ /\S/; 39 | my $row; 40 | if ($row0 =~ /^\s*\|--+(?:\+--+)*\|?\s*$/) { 41 | $row = Org::Element::TableHLine->new(parent => $self); 42 | } elsif ($row0 =~ /^\s*\|\s*(.+?)\s*\|?\s*$/) { 43 | my $s = $1; 44 | $row = Org::Element::TableRow->new( 45 | parent => $self, children=>[]); 46 | for my $cell0 (split /\s*\|\s*/, $s) { 47 | my $cell = Org::Element::TableCell->new( 48 | parent => $row, children=>[]); 49 | $doc->_add_text($cell0, $cell, $pass); 50 | push @{ $row->children }, $cell; 51 | } 52 | } else { 53 | $self->die("Invalid line in table: $row0"); 54 | } 55 | push @{$self->children}, $row; 56 | } 57 | } 58 | } 59 | 60 | sub rows { 61 | my ($self) = @_; 62 | return [] unless $self->children; 63 | my $rows = []; 64 | for my $el (@{$self->children}) { 65 | push @$rows, $el if $el->isa('Org::Element::TableRow'); 66 | } 67 | $rows; 68 | } 69 | 70 | sub row_count { 71 | my ($self) = @_; 72 | return 0 unless $self->children; 73 | my $n = 0; 74 | for my $el (@{$self->children}) { 75 | $n++ if $el->isa('Org::Element::TableRow'); 76 | } 77 | $n; 78 | } 79 | 80 | sub column_count { 81 | my ($self) = @_; 82 | return 0 unless $self->children; 83 | 84 | # get first row 85 | my $row; 86 | for my $el (@{$self->children}) { 87 | if ($el->isa('Org::Element::TableRow')) { 88 | $row = $el; 89 | last; 90 | } 91 | } 92 | return 0 unless $row; # table doesn't have any row 93 | 94 | my $n = 0; 95 | for my $el (@{$row->children}) { 96 | $n++ if $el->isa('Org::Element::TableCell'); 97 | } 98 | $n; 99 | } 100 | 101 | sub as_aoa { 102 | my ($self) = @_; 103 | return [] unless $self->children; 104 | 105 | my @rows; 106 | for my $row (@{$self->children}) { 107 | next unless $row->isa('Org::Element::TableRow'); 108 | push @rows, $row->as_array; 109 | } 110 | \@rows; 111 | } 112 | 113 | 1; 114 | # ABSTRACT: Represent Org table 115 | 116 | =for Pod::Coverage BUILD 117 | 118 | =head1 DESCRIPTION 119 | 120 | Derived from L. Must have L or 121 | L instances as its children. 122 | 123 | 124 | =head1 ATTRIBUTES 125 | 126 | 127 | =head1 METHODS 128 | 129 | =head2 $table->rows() => ELEMENTS 130 | 131 | Return the rows of the table. 132 | 133 | =head2 $table->as_aoa() => ARRAY 134 | 135 | Return the rows of the table, each row already an arrayref of cells produced 136 | using as_array() method. Horizontal lines will be skipped/ignored. 137 | 138 | =head2 $table->row_count() => INT 139 | 140 | Return the number of rows that the table has. 141 | 142 | =head2 $table->column_count() => INT 143 | 144 | Return the number of columns that the table has. It is counted from the first 145 | row. 146 | 147 | =cut 148 | -------------------------------------------------------------------------------- /lib/Org/Element/TableCell.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::TableCell; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | 8 | # AUTHORITY 9 | # DATE 10 | # DIST 11 | # VERSION 12 | 13 | 1; 14 | # ABSTRACT: Represent Org table cell 15 | 16 | =head1 DESCRIPTION 17 | 18 | Derived from L. 19 | 20 | 21 | =head1 ATTRIBUTES 22 | 23 | 24 | =head1 METHODS 25 | 26 | =cut 27 | -------------------------------------------------------------------------------- /lib/Org/Element/TableHLine.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::TableHLine; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | 8 | # AUTHORITY 9 | # DATE 10 | # DIST 11 | # VERSION 12 | 13 | sub as_string { 14 | my ($self) = @_; 15 | return $self->_str if $self->_str; 16 | "|---\n"; 17 | } 18 | 19 | 1; 20 | #ABSTRACT: Represent Org table horizontal line 21 | 22 | =head1 DESCRIPTION 23 | 24 | Derived from L. 25 | 26 | 27 | =head1 ATTRIBUTES 28 | 29 | 30 | =head1 METHODS 31 | 32 | =for Pod::Coverage as_string 33 | 34 | =cut 35 | -------------------------------------------------------------------------------- /lib/Org/Element/TableRow.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::TableRow; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | 8 | # AUTHORITY 9 | # DATE 10 | # DIST 11 | # VERSION 12 | 13 | sub as_string { 14 | my ($self) = @_; 15 | return $self->_str if defined $self->_str; 16 | 17 | join("", 18 | "|", 19 | join("|", map {$_->as_string} @{$self->children}), 20 | "\n"); 21 | } 22 | 23 | sub as_array { 24 | my ($self) = @_; 25 | 26 | [map {$_->as_string} @{$self->children}]; 27 | } 28 | 29 | sub cells { 30 | my ($self) = @_; 31 | return [] unless $self->children; 32 | 33 | my $cells = []; 34 | for my $el (@{$self->children}) { 35 | push @$cells, $el if $el->isa('Org::Element::TableCell'); 36 | } 37 | $cells; 38 | } 39 | 40 | 1; 41 | # ABSTRACT: Represent Org table row 42 | 43 | =for Pod::Coverage as_string 44 | 45 | =head1 DESCRIPTION 46 | 47 | Derived from L. Must have L 48 | instances as its children. 49 | 50 | 51 | =head1 ATTRIBUTES 52 | 53 | 54 | =head1 METHODS 55 | 56 | =head2 $table->cells() => ELEMENTS 57 | 58 | Return the cells of the row. 59 | 60 | =head2 $table->as_array() => ARRAY 61 | 62 | Return an arrayref containing the cells of the row, each cells already 63 | stringified with as_string(). 64 | 65 | =cut 66 | -------------------------------------------------------------------------------- /lib/Org/Element/Target.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Target; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | with 'Org::ElementRole'; 8 | with 'Org::ElementRole::Inline'; 9 | 10 | # AUTHORITY 11 | # DATE 12 | # DIST 13 | # VERSION 14 | 15 | has target => (is => 'rw'); 16 | 17 | sub as_string { 18 | my ($self) = @_; 19 | join("", 20 | "<<", ($self->target // ""), ">>"); 21 | } 22 | 23 | sub as_text { 24 | goto \&as_string; 25 | } 26 | 27 | 1; 28 | # ABSTRACT: Represent Org target 29 | 30 | =head1 DESCRIPTION 31 | 32 | Derived from L. 33 | 34 | 35 | =head1 ATTRIBUTES 36 | 37 | =head2 target 38 | 39 | 40 | =head1 METHODS 41 | 42 | =head2 as_string => str 43 | 44 | From L. 45 | 46 | =head2 as_text => str 47 | 48 | From L. 49 | 50 | =cut 51 | -------------------------------------------------------------------------------- /lib/Org/Element/Text.pm: -------------------------------------------------------------------------------- 1 | package Org::Element::Text; 2 | 3 | use 5.010; 4 | use locale; 5 | use Moo; 6 | extends 'Org::Element'; 7 | with 'Org::ElementRole'; 8 | with 'Org::ElementRole::Inline'; 9 | 10 | # AUTHORITY 11 | # DATE 12 | # DIST 13 | # VERSION 14 | 15 | has text => (is => 'rw'); 16 | has style => (is => 'rw'); 17 | 18 | our %mu2style = (''=>'', '*'=>'B', '_'=>'U', '/'=>'I', 19 | '+'=>'S', '='=>'C', '~'=>'V'); 20 | our %style2mu = reverse(%mu2style); 21 | 22 | sub as_string { 23 | my ($self) = @_; 24 | my $muchar = $style2mu{$self->style // ''} // ''; 25 | 26 | join("", 27 | $muchar, 28 | $self->text // '', $self->children_as_string, 29 | $muchar); 30 | } 31 | 32 | sub as_text { 33 | my $self = shift; 34 | my $muchar = $style2mu{$self->style // ''} // ''; 35 | 36 | join("", 37 | $muchar, 38 | $self->text // '', $self->children_as_text, 39 | $muchar); 40 | } 41 | 42 | 1; 43 | # ABSTRACT: Represent text 44 | 45 | =for Pod::Coverage as_string 46 | 47 | =head1 DESCRIPTION 48 | 49 | Derived from L. 50 | 51 | Org::Element::Text is an object that represents a piece of text. It has C 52 | and C