├── .github └── workflows │ └── test.yml ├── .gitignore ├── .recipes ├── helm-org-multi-wiki └── org-multi-wiki ├── LICENSE ├── README.org ├── helm-org-multi-wiki.el ├── notes.org ├── org-multi-wiki-tests.el ├── org-multi-wiki.el └── screenshots └── helm-org-multi-wiki-multi-ns-1.png /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**.md' 9 | - '**.org' 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: cachix/install-nix-action@v12 15 | with: 16 | nix_path: nixpkgs=channel:nixos-unstable 17 | - uses: actions/checkout@v2 18 | - uses: akirak/elinter@v4 19 | - run: elinter -t --buttercup -e latest 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by pre-commit.nix via elinter -g 2 | .pre-commit-config.yaml 3 | 4 | .elinter-cache 5 | -------------------------------------------------------------------------------- /.recipes/helm-org-multi-wiki: -------------------------------------------------------------------------------- 1 | (helm-org-multi-wiki :fetcher github :repo "akirak/org-multi-wiki" 2 | :files ("helm-org-multi-wiki.el")) -------------------------------------------------------------------------------- /.recipes/org-multi-wiki: -------------------------------------------------------------------------------- 1 | (org-multi-wiki :fetcher github :repo "akirak/org-multi-wiki" 2 | :files (:defaults (:exclude "helm-org-multi-wiki.el"))) 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | # -*- mode: org; mode: org-make-toc -*- 2 | * org-multi-wiki 3 | This is a package for working with multiple Org-based knowledge base sites inside a single Emacs session. 4 | It uses alphapapa's [[https://github.com/alphapapa/org-ql][helm-org-ql]] for searching, which makes navigation and entry creation seamless. 5 | 6 | #+BEGIN_HTML 7 | 8 | Build Status 9 | 10 | 11 | org-multi-wiki on MELPA 12 | 13 | helm-org-multi-wiki on MELPA 14 | #+END_HTML 15 | ** Table of contents 16 | :PROPERTIES: 17 | :TOC: siblings 18 | :END: 19 | - [[#features][Features]] 20 | - [[#prerequisites][Prerequisites]] 21 | - [[#installation][Installation]] 22 | - [[#configuration][Configuration]] 23 | - [[#namespace-predicate-for-org-ql][Namespace predicate for org-ql]] 24 | - [[#integration-with-org-recent-headings][Integration with org-recent-headings]] 25 | - [[#usage][Usage]] 26 | - [[#the-current-namespace][The current namespace]] 27 | - [[#helm-org-multi-wiki][helm-org-multi-wiki]] 28 | - [[#running-on-particular-namespaces][Running on particular namespaces]] 29 | - [[#quickly-creating-an-entry-in-a-particular-namespace][Quickly creating an entry in a particular namespace]] 30 | - [[#inserting-a-link][Inserting a link]] 31 | - [[#moving-contents][Moving contents]] 32 | - [[#details][Details]] 33 | - [[#wiki-link-type][wiki link type]] 34 | - [[#file-name-escaping][File name escaping]] 35 | - [[#linking-to-a-particular-heading-in-a-file][Linking to a particular heading in a file]] 36 | - [[#entry-levels-and-entry-templates][Entry levels and entry templates]] 37 | - [[#buffer-names][Buffer names]] 38 | - [[#customizing-the-helm-source][Customizing the helm source]] 39 | - [[#alternatives][Alternatives]] 40 | - [[#license][License]] 41 | 42 | ** Features 43 | =org-multi-wiki= is an Org-based wiki system for Emacs with the following features: 44 | 45 | - It supports working with multiple isolated directories inside a single Emacs session. It lets you define namespaces to uniquely identify content groups. You can search contents from one or more of the namespaces and create a new entry in any of them. 46 | - It supports =org-ql= for searching contents in wikis. =org-ql= is an expressive query language designed for Org mode. A helm support is included. 47 | - It adds a custom =wiki= link type to =org-mode= to resolve locations of wiki files across namespaces. 48 | - The directory of each namespace can be either recursive or non-recursive. If it's set to recursive, =org-multi-wiki= also searches Org files in subdirectories recursively. 49 | ** Prerequisites 50 | - Emacs 26.1 or later 51 | - Org 9.3 or later 52 | - =dash.el= 53 | - =s.el= 54 | - =org-ql= 55 | - [[https://github.com/alphapapa/org-ql][helm-org-ql]], if you use helm-org-multi-wiki (recommended) 56 | ** Installation 57 | This package is available on MELPA as =org-multi-wiki= and =helm-org-multi-wiki=. 58 | ** Configuration 59 | Below is an example configuration through =use-package=: 60 | 61 | #+begin_src emacs-lisp 62 | (use-package org-multi-wiki 63 | :config 64 | (org-multi-wiki-global-mode 1) 65 | :custom 66 | (org-multi-wiki-namespace-list '((personal "~/org/personal/") 67 | (ops "~/org/ops/") 68 | (programming "~/org/programming/") 69 | (client1 "~/projects/client1/client1-docs/") 70 | (client2 "~/projects/client2/client2-docs/"))) 71 | ;; Namespace of a wiki 72 | (org-multi-wiki-default-namespace 'personal)) 73 | 74 | (use-package helm-org-multi-wiki) 75 | #+end_src 76 | 77 | First configure =org-multi-wiki-namespace-list= variable. 78 | It is a list where each item is a list of the following items: 79 | 80 | - A symbol to identify the wiki, called namespace 81 | - A directory containing Org files. 82 | 83 | You can append a plist to the list to set options on each namespace. 84 | 85 | You also have to turn on =org-multi-wiki-global-mode= for activating several function advices and the custom link type. 86 | 87 | For more options, see =org-multi-wiki= customization group. 88 | *** Namespace predicate for org-ql 89 | :PROPERTIES: 90 | :CREATED_TIME: [2020-12-12 Sat 23:36] 91 | :END: 92 | You can also define a predicate for =org-ql= to test if an entry is inside a particular namespace: 93 | 94 | #+begin_src emacs-lisp 95 | (org-ql-defpred wiki (namespace) 96 | "It is inside a particular namespace." 97 | :body (org-multi-wiki-in-namespace-p (intern namespace))) 98 | #+end_src 99 | 100 | You can use the predicate in =helm-org-multi-wiki= to display entries in a particular namespace, e.g. =wiki:namespace keyword=. 101 | *** Integration with org-recent-headings 102 | :PROPERTIES: 103 | :CREATED_TIME: [2020-12-12 Sat 23:37] 104 | :END: 105 | [[https://github.com/alphapapa/org-recent-headings/][org-recent-headings]] is a package which lets you quickly jump to a recently visited heading. 106 | To track headings visited by =org-multi-wiki=, you may want to add the following configuration: 107 | 108 | #+begin_src emacs-lisp 109 | (with-eval-after-load 'org-recent-headings 110 | (add-hook 'org-recent-headings-advise-functions 111 | #'org-multi-wiki-follow-link) 112 | (add-hook 'org-recent-headings-advise-functions 113 | #'helm-org-ql-show-marker) 114 | (add-hook 'org-recent-headings-advise-functions 115 | #'helm-org-ql-show-marker-indirect)) 116 | #+end_src 117 | ** Usage 118 | *** The current namespace 119 | Most commands in this package work with a single namespace: the current namespace. 120 | 121 | To open a file in the current namespace, use =org-multi-wiki-visit-entry= command. 122 | To switch the namespace, use =org-multi-wiki-switch=. 123 | 124 | Optionally, you can work on a specific namespace by calling functions with a namespace. 125 | For example, =org-multi-wiki-visit-entry= with a universal prefix argument (~C-u~) lets you select a namespace. 126 | *** helm-org-multi-wiki 127 | =helm-org-multi-wiki= command is the main entry point in this package. 128 | It uses [[https://github.com/alphapapa/org-ql#helm-org-ql][helm-org-ql]] for search and a dummy source for entry creation. 129 | It is included in =helm-org-multi-wiki.el=. 130 | 131 | By default, it lets you select a heading or create a new entry in the current namespace. 132 | 133 | With a universal prefix argument (~C-u~), you can select wikis. 134 | You can select multiple namespaces with ~C-SPC~. 135 | **** Running on particular namespaces 136 | :PROPERTIES: 137 | :CREATED_TIME: [2020-02-22 Sat 14:34] 138 | :END: 139 | You can also use it as a function which accepts a namespace or a list of namespaces. 140 | You can define your own command to search queries in a specific wiki. 141 | 142 | #+begin_src emacs-lisp 143 | (defun helm-org-multi-wiki-project1 () 144 | (interactive) 145 | (helm-org-multi-wiki '(project1))) 146 | #+end_src 147 | 148 | When multiple namespaces are given, you can select a directory in which you want to create a new file. 149 | This is available as alternative actions (~tab~) in the dummy source of Helm. 150 | 151 | The package also provides =helm-org-multi-wiki-all=, which performs search on all namespaces. 152 | **** Quickly creating an entry in a particular namespace 153 | :PROPERTIES: 154 | :CREATED_TIME: [2020-02-22 Sat 14:35] 155 | :END: 156 | The dummy source has a dedicated keymap =helm-make-helm-org-multi-wiki-dummy-source-map= which lets you bind keys to create an entry in a particular namespace: 157 | 158 | #+begin_src emacs-lisp 159 | (general-def :keymap 'helm-org-multi-wiki-dummy-source-map :package 'helm-org-multi-wiki 160 | :prefix "C-c C-c" 161 | "p" (helm-org-multi-wiki-def-create-entry-action programming)) 162 | #+end_src 163 | 164 | With this configuration, you can create an entry in =programming= wiki from the minibuffer input by pressing ~C-c C-c p~. 165 | 166 | =helm-org-multi-wiki-def-create-entry-action= macro defines an interactive function which exits the running helm session and visits an entry. 167 | **** Inserting a link 168 | :PROPERTIES: 169 | :CREATED_TIME: [2021-02-09 Tue 00:06] 170 | :END: 171 | =helm-org-multi-wiki-insert-link= lets you select a heading from all namespaces and inserts a link to the heading. 172 | 173 | If there is an active region, it replaces the selected text with a link. 174 | *** Moving contents 175 | You can move an existing subtree to an wiki using =org-multi-wiki-create-entry-from-subtree= command. 176 | ** Details 177 | *** =wiki= link type 178 | :PROPERTIES: 179 | :CREATED_TIME: [2020-02-09 Sun 16:01] 180 | :END: 181 | This package adds =wiki= link type to =org-link-parameters=. 182 | 183 | The link format complies to one of the following formats: 184 | 185 | - Linking with a custom ID: =NAMESPACE:[subdir/]TITLE[::#customid]= 186 | - Linking with a heading: =NAMESPACE:[subdir/]TITLE[::*heading]= 187 | 188 | =NAMESPACE= is the namespace of a wiki. It is omitted when linking to the same namespace. 189 | Note: This behaviour can be altered by setting =org-multi-wiki-allow-omit-namespace= to nil. 190 | 191 | =TITLE= can be either the base name of an escaped file name (i.e. without =.org=) or its original top-level heading. 192 | =::= and its following part is omitted when linking to a top-level heading in a file. 193 | 194 | File resolution is done by attempting the following schemes, in that order: 195 | 196 | 1. It tries to find a file with the base name of the exact =TITLE=. 197 | 2. It escapes =TITLE= into a safe file name and tries to find a file with the base name. 198 | 3. It tries to find a file with a top-level heading matching =TITLE= in the directory. 199 | 200 | =subdir= is not supported now, but it will be added when this package supports recursive file search. 201 | *** File name escaping 202 | :PROPERTIES: 203 | :CREATED_TIME: [2020-03-14 Sat 20:04] 204 | :END: 205 | =org-multi-wiki= does some escaping of file names. 206 | The escaping function consists of multiple steps such as: 207 | 208 | - Split the title by whitespace, capitalize each word, and concatenate them. The result is usually upper camel cased. 209 | - If a word contains at least one upper case alphabet, the word is not capitalized and case is retained. 210 | - It eliminates symbols other than hyphens, dots, and underscores. 211 | - It eliminates words such as "a", "an", and "the". 212 | - It keeps non-ascii characters such as Chinese and Japanese. 213 | 214 | It is designed to be both filename-safe and friendly to the modern world with technical terms. 215 | However, you can alter the logic by setting =org-multi-wiki-escape-file-name-fn= to another function. 216 | *** Linking to a particular heading in a file 217 | You can link to a heading in a file either with a heading text or with a custom ID property. 218 | 219 | Linking with a custom ID is generally safer, because custom IDs don't change when you change headings. 220 | To enforce generation of a custom ID when storing a link, set =org-multi-wiki-want-custom-id= variable to t. 221 | 222 | Note that a link to a top-level heading does not contain a link fragment, i.e. a heading or a custom ID by default. 223 | See the following subsection for changing this behaviour. 224 | *** Entry levels and entry templates 225 | It is recommended that you include at least one heading in each Org file in wiki. 226 | The following structure is not recommended: 227 | 228 | #+begin_example 229 | #+title: My wiki page 230 | \* First heading 231 | #+end_example 232 | 233 | Instead, the following structure is recommended: 234 | 235 | #+begin_example 236 | \* My wiki page 237 | \** First heading 238 | #+end_example 239 | 240 | I write READMEs of my open source projects in this style, and if you use =helm-org-ql=, you won't be able to reach a file without a heading. 241 | 242 | Following this principle, the default file template of this package generates a heading rather than a file header. 243 | To change the template, set =org-multi-wiki-entry-template-fn= variable.. 244 | 245 | Also, links to top-level heading don't contain a link fragment by default. 246 | This is because top-level headings are considered page titles in the structure and each file should contain only one top-level heading. 247 | However, depending on your needs, you may want to include multiple top-level headings in a single file. 248 | You can include a fragment in a link to a top-level heading using one of the following options: 249 | 250 | - Set =org-multi-wiki-top-level-link-fragments= to t, which is globally effective 251 | - Set =:top-level-link-fragments= option in =org-multi-wiki-namespace-list=, which is locally effective 252 | *** Buffer names 253 | :PROPERTIES: 254 | :CREATED_TIME: [2020-03-24 Tue 00:53] 255 | :END: 256 | To let the user easily distinguish between wikis, =org-multi-wiki= renames file buffers according to their respective namespaces when it opens Org files: 257 | 258 | [[file:screenshots/helm-org-multi-wiki-multi-ns-1.png]] 259 | 260 | To turn off this behavior, set =org-multi-wiki-rename-buffer= to nil. 261 | *** Customizing the helm source 262 | :PROPERTIES: 263 | :CREATED_TIME: [2020-03-24 Tue 00:53] 264 | :END: 265 | Although =helm-org-multi-wiki= is based on =helm-org-ql=, it allows further customizations to make it slightly different from the original package: 266 | 267 | - It can display items when no query is given in the minibuffer. By default, it displays top-level items. You can customize this via =helm-org-multi-wiki-default-query= variable. This should be an S-expression query accepted by =org-ql=. 268 | - It allows you to customize the query parser by setting =helm-org-multi-wiki-query-parser= to a different value. By default, it uses the plain query parser of =org-ql=. 269 | - You can change the keymap and the action by setting =helm-org-multi-wiki-map= and =helm-org-ql-actions=, respectively, By default, it uses the same values as =helm-org-ql=. 270 | ** Alternatives 271 | There are several knowledge base systems for Emacs based on Org mode. 272 | 273 | [[https://github.com/Kungsgeten/org-brain][org-brain]] and [[https://github.com/jethrokuan/org-roam][org-roam]] are especially powerful ones. 274 | org-brain is based on the idea of concept mapping, and org-roam is a rudimentary replica of another software named Roam. 275 | org-multi-wiki is not based on such a specific framework. 276 | It focuses on search and entry creation and has built-in support for multiple namespaces. 277 | It provides an infrastructure for building your own wiki system on top of Org mode. 278 | 279 | [[https://github.com/abo-abo/plain-org-wiki][plain-org-wiki]] is the direct inspiration of this package. 280 | org-multi-wiki supports multiple namespaces and takes advantage of =helm= and =org-ql= for providing a rich querying interface. 281 | ** License 282 | GPLv3 283 | -------------------------------------------------------------------------------- /helm-org-multi-wiki.el: -------------------------------------------------------------------------------- 1 | ;;; helm-org-multi-wiki.el --- Helm interface to org-multi-wiki -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2020 Akira Komamura 4 | 5 | ;; Author: Akira Komamura 6 | ;; Version: 0.3.6 7 | ;; Package-Requires: ((emacs "26.1") (org "9.3") (org-multi-wiki "0.4") (org-ql "0.5") (dash "2.18") (helm-org-ql "0.5") (helm "3.5")) 8 | ;; Keywords: org, outlines 9 | ;; URL: https://github.com/akirak/org-multi-wiki 10 | 11 | ;; This file is not part of GNU Emacs. 12 | 13 | ;;; License: 14 | 15 | ;; This program is free software: you can redistribute it and/or modify 16 | ;; it under the terms of the GNU General Public License as published by 17 | ;; the Free Software Foundation, either version 3 of the License, or 18 | ;; (at your option) any later version. 19 | ;; 20 | ;; This program is distributed in the hope that it will be useful, 21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | ;; GNU General Public License for more details. 24 | ;; 25 | ;; You should have received a copy of the GNU General Public License 26 | ;; along with this program. If not, see . 27 | 28 | ;;; Commentary: 29 | 30 | ;; This package provides a helm interface to org-multi-wiki. 31 | 32 | ;;; Code: 33 | 34 | (require 'dash) 35 | (require 'org-multi-wiki) 36 | (require 'org-ql) 37 | (require 'helm) 38 | (require 'helm-org-ql) 39 | (require 'ol) 40 | 41 | ;; Silence byte-compiler 42 | (defvar helm-map) 43 | (defvar helm-input-idle-delay) 44 | (defvar helm-org-ql-input-idle-delay) 45 | (defvar helm-org-ql-map) 46 | 47 | (defgroup helm-org-multi-wiki nil 48 | "Helm interface to org-multi-wiki." 49 | :group 'org-multi-wiki 50 | :group 'helm) 51 | 52 | (defvar helm-org-multi-wiki-dummy-source-map 53 | (let ((map (make-composed-keymap nil helm-map))) 54 | map) 55 | "Keymap for the dummy source.") 56 | 57 | (defcustom helm-org-multi-wiki-show-files t 58 | "Whether to prepend file list in `helm-org-multi-wiki'." 59 | :type 'boolean) 60 | 61 | (defcustom helm-org-multi-wiki-default-namespace nil 62 | "Default namespace for creating a new file. 63 | 64 | This option determines which namespace in 65 | `org-multi-wiki-namespace-list' will be the target of the default 66 | action in Helm dummy sources in this package. 67 | 68 | This should be the symbol for a namespace in 69 | `org-multi-wiki-namespace-list' or nil. 70 | 71 | If it is nil, `org-multi-wiki-current-namespace' will be the default. 72 | 73 | Alternatively, you can select other namespaces by pressing TAB in 74 | the Helm sources." 75 | :type '(choice nil symbol)) 76 | 77 | (defcustom helm-org-multi-wiki-namespace-actions 78 | (quote (("Switch" . org-multi-wiki-switch) 79 | ("Search in namespace(s)" 80 | . (lambda (ns) 81 | (helm-org-multi-wiki (or (helm-marked-candidates) ns)))))) 82 | "Alist of actions in `helm-org-multi-wiki-namespace'." 83 | :type '(alist :key-type string :value-type (or symbol function))) 84 | 85 | (defcustom helm-org-multi-wiki-namespace-persistent-action 86 | ;; TODO: Add a persistent action for namespace 87 | nil 88 | "Persistent action in `helm-org-multi-wiki-namespace'." 89 | :type 'function) 90 | 91 | (defcustom helm-org-multi-wiki-skip-subtrees t 92 | "Whether to skip subtrees matching the query for cleaner output." 93 | :type 'boolean) 94 | 95 | (defcustom helm-org-multi-wiki-create-entry-function 96 | #'org-multi-wiki-visit-entry 97 | "Function used to create a new entry from the dummy source. 98 | 99 | This function should accept the following arguments: 100 | 101 | (func TITLE :namespace NAMESPACE) 102 | 103 | where TITLE is the name of the new entry and NAMESPACE is a 104 | symbol to denote the namespace. See `org-multi-wiki-visit-entry' 105 | for an example, which is the default value." 106 | :type 'function) 107 | 108 | (defcustom helm-org-multi-wiki-insert-link-actions 109 | '(("Insert a link" . helm-org-multi-wiki--insert-link) 110 | ("Insert a link (modify the label)" . helm-org-multi-wiki--insert-link-with-label)) 111 | "Alist of actions used to insert a link to a heading." 112 | :type 'alist) 113 | 114 | (defmacro helm-org-multi-wiki-with-namespace-buffers (namespaces &rest progn) 115 | "Evaluate an expression with namespace buffers. 116 | 117 | This macro sets `helm-org-multi-wiki-buffers` to buffers from NAMESPACES 118 | and evaluate PROGN." 119 | (declare (indent 1)) 120 | `(progn 121 | (setq helm-org-multi-wiki-buffers 122 | (->> ,namespaces 123 | (--map (org-multi-wiki-entry-files it :as-buffers t)) 124 | (apply #'append))) 125 | ,@progn)) 126 | 127 | (defsubst helm-org-multi-wiki--create-entry (namespace title) 128 | "In NAMESPACE, create a new entry from TITLE." 129 | (funcall helm-org-multi-wiki-create-entry-function title :namespace namespace)) 130 | 131 | (defun helm-org-multi-wiki-create-entry-from-input (namespace) 132 | "Create an entry in NAMESPACE from the input in the dummy source." 133 | (let ((title (helm-get-selection))) 134 | (if (not (string-empty-p title)) 135 | (helm-run-after-exit #'helm-org-multi-wiki--create-entry namespace title) 136 | (user-error "Input is empty")))) 137 | 138 | ;;;###autoload 139 | (defmacro helm-org-multi-wiki-def-create-entry-action (namespace) 140 | "Define a command to create an entry in NAMESPACE via the dummy source. 141 | 142 | This function is only provided as a utility." 143 | `(defun ,(intern (format "helm-org-multi-wiki-create/%s" namespace)) () 144 | (interactive) 145 | (helm-org-multi-wiki-create-entry-from-input (quote ,namespace)))) 146 | 147 | (defun helm-org-multi-wiki--insert-link (marker &optional modify-label) 148 | "Insert a link to a heading. 149 | 150 | MARKER is the marker to the link target. 151 | 152 | If MODIFY-LABEL is non-nil, it prompts for the link text." 153 | (let* ((plist (org-with-point-at marker 154 | ;; TODO: Pass the origin-ns as an argument 155 | (org-multi-wiki--get-link-data))) 156 | (headline (plist-get plist :headline)) 157 | (link-text (if modify-label 158 | (read-string "Link label: " headline) 159 | headline))) 160 | (helm-org-multi-wiki--make-link-dwim (plist-get plist :link) 161 | link-text))) 162 | 163 | (defun helm-org-multi-wiki--insert-link-with-label (marker) 164 | "Insert a link to a heading, with the link text modified. 165 | 166 | MARKER is the marker to the link target." 167 | (helm-org-multi-wiki--insert-link marker t)) 168 | 169 | (defun helm-org-multi-wiki--insert-new-entry-link (namespace title) 170 | "Insert a link to a non-existent entry. 171 | 172 | NAMESPACE is the namespace in which a new entry will be created, 173 | and TITLE is the title of the entry." 174 | (-> (org-multi-wiki--make-link namespace title :to-file t) 175 | (helm-org-multi-wiki--make-link-dwim title))) 176 | 177 | (defun helm-org-multi-wiki--link-info-at-point () 178 | "Return information on the link at point if any." 179 | (when-let (plist0 (get-char-property (point) 'htmlize-link)) 180 | (save-match-data 181 | (pcase (org-in-regexp org-link-any-re 2) 182 | (`(,begin . ,end) 183 | (let ((raw (buffer-substring-no-properties begin end))) 184 | (append (list :begin begin :end end 185 | :text (when (string-match org-link-bracket-re raw) 186 | (match-string 2 raw))) 187 | ;; Contains :uri 188 | plist0))))))) 189 | 190 | (defun helm-org-multi-wiki--verbatim-info-at-point () 191 | "Return information on the verbatim at point if any." 192 | (save-match-data 193 | (pcase (org-in-regexp org-verbatim-re 2) 194 | (`(,begin . ,end) 195 | (list :begin begin :end end 196 | :text (buffer-substring-no-properties (match-end 3) (match-end 4))))))) 197 | 198 | (defun helm-org-multi-wiki--link-context () 199 | "Identify the thing at point for linking." 200 | (or (when (region-active-p) 201 | (let* ((begin (region-beginning)) 202 | (end (region-end)) 203 | (text (buffer-substring-no-properties begin end))) 204 | (list 'region :begin begin :end end :text text))) 205 | (-some->> (helm-org-multi-wiki--link-info-at-point) 206 | (cons 'link)) 207 | (-some->> (helm-org-multi-wiki--verbatim-info-at-point) 208 | (cons 'verbatim)))) 209 | 210 | (defun helm-org-multi-wiki--make-link-dwim (link text) 211 | "Produce an Org link depending on the context. 212 | 213 | When there is an active region, replace the selected text with a 214 | LINK with the original TEXT as the label. The second argument 215 | will be discarded. 216 | 217 | When the point is on a link/verbatim, replace it with a link, 218 | maintaining its text. 219 | 220 | Otherwise, it inserts a link to LINK with TEXT as the label." 221 | (if-let (context (helm-org-multi-wiki--link-context)) 222 | (-let (((&plist :begin :end) (cdr context))) 223 | (delete-region begin end) 224 | (goto-char begin) 225 | (insert (org-link-make-string link (or (plist-get (cdr context) :text) 226 | text)))) 227 | (insert (org-link-make-string link text)))) 228 | 229 | (defun helm-org-multi-wiki-file-link-insert-action (buffer) 230 | "Insert a link to BUFFER, with its first heading as the link text." 231 | (-let (((plist headline) (with-current-buffer buffer 232 | (org-with-wide-buffer 233 | (goto-char (point-min)) 234 | (list (org-multi-wiki-entry-file-p) 235 | (when (re-search-forward org-heading-regexp nil t) 236 | (org-get-heading t t t t))))))) 237 | (helm-org-multi-wiki--make-link-dwim (org-multi-wiki--make-link 238 | (plist-get plist :namespace) 239 | (plist-get plist :basename) 240 | :to-file t) 241 | (or headline 242 | (plist-get plist :basename))))) 243 | 244 | (defsubst helm-org-multi-wiki--format-ns-cand (x) 245 | "Format a helm candidate label of a namespace entry X." 246 | (pcase-let ((`(,ns ,root . _) x)) 247 | (format "%s (%s)" ns root))) 248 | 249 | (defclass helm-org-multi-wiki-source-namespace-symbol (helm-source-sync) 250 | ((candidates 251 | :initform (lambda () 252 | (-map (lambda (x) 253 | (cons (helm-org-multi-wiki--format-ns-cand x) 254 | (car x))) 255 | org-multi-wiki-namespace-list))) 256 | (persistent-action :initform 'helm-org-multi-wiki-namespace-persistent-action))) 257 | 258 | ;; Like `helm-org-multi-wiki-source-namespace-symbol' in the above, 259 | ;; but returns the whole alist entry. 260 | (defclass helm-org-multi-wiki-source-namespace-entry (helm-source-sync) 261 | ((candidates 262 | :initform (lambda () 263 | (-map (lambda (x) 264 | (cons (helm-org-multi-wiki--format-ns-cand x) 265 | x)) 266 | org-multi-wiki-namespace-list))))) 267 | 268 | (defun helm-org-multi-wiki--normalize-namespaces (namespaces) 269 | "Normalize NAMESPACES, i.e. convert to a list." 270 | (cl-etypecase namespaces 271 | ;; Normalize namespaces to make it a list of symbols. 272 | (null (if org-multi-wiki-current-namespace 273 | (list org-multi-wiki-current-namespace) 274 | (let ((namespaces (helm-org-multi-wiki-namespace 275 | :prompt "Switch to a namespace: "))) 276 | (unless namespaces 277 | (user-error "Please select a namespace")) 278 | (org-multi-wiki-switch (car-safe namespaces)) 279 | namespaces))) 280 | (list namespaces) 281 | (symbol (list namespaces)))) 282 | 283 | (cl-defun helm-org-multi-wiki-namespace (&key prompt action) 284 | "Select directory namespaces using helm. 285 | 286 | PROMPT and ACTION are passed to helm." 287 | (interactive) 288 | (let ((prompt (or prompt "org-multi-wiki namespaces: ")) 289 | (action (or action 290 | (if (called-interactively-p 'any) 291 | helm-org-multi-wiki-namespace-actions 292 | (lambda (candidate) 293 | (or (helm-marked-candidates) candidate)))))) 294 | (helm :prompt prompt 295 | :sources 296 | (helm-make-source "Wiki namespace" 297 | 'helm-org-multi-wiki-source-namespace-symbol 298 | :action action)))) 299 | 300 | (defvar helm-org-multi-wiki-buffers nil) 301 | 302 | (defvar helm-org-multi-wiki-map 303 | (make-composed-keymap nil helm-org-ql-map)) 304 | 305 | (defcustom helm-org-multi-wiki-actions nil 306 | "Alist of actions in `helm-org-multi-wiki'. 307 | 308 | This can be nil. In that case, `helm-org-ql-actions' will be 309 | inherited." 310 | :type 'alist) 311 | 312 | (defcustom helm-org-multi-wiki-file-actions 313 | '(("Switch to the buffer" . switch-to-buffer) 314 | ("Switch to the buffer (other window)" . switch-to-buffer-other-window) 315 | ("Switch to the buffer (other frame)" . switch-to-buffer-other-frame)) 316 | "Helm actions for Org file buffers." 317 | :type 'alist) 318 | 319 | (defcustom helm-org-multi-wiki-default-query '(level 1) 320 | "Query sent when no input is in the minibuffer." 321 | :type 'sexp) 322 | 323 | (defcustom helm-org-multi-wiki-query-parser 324 | ;; This is an internal API of org-ql, so it would be better to avoid it 325 | #'org-ql--query-string-to-sexp 326 | "Function used to parse the plain query. 327 | 328 | The function should take a plain query of org-ql.el as the argument 329 | and return an S expression query." 330 | :type 'function) 331 | 332 | ;; Based on `helm-org-ql-source' from helm-org-ql.el at 0.5-pre. 333 | (defclass helm-org-multi-wiki-source (helm-source-sync) 334 | ((candidates :initform (lambda () 335 | (let* ((query (if (string-empty-p helm-pattern) 336 | helm-org-multi-wiki-default-query 337 | (funcall helm-org-multi-wiki-query-parser helm-pattern))) 338 | (window-width (window-width (helm-window)))) 339 | (when query 340 | (with-current-buffer (helm-buffer-get) 341 | (setq helm-org-ql-buffers-files helm-org-multi-wiki-buffers)) 342 | (ignore-errors 343 | ;; Ignore errors that might be caused by partially typed queries. 344 | (org-ql-select helm-org-multi-wiki-buffers query 345 | :action `(prog1 346 | (helm-org-ql--heading ,window-width) 347 | (when helm-org-multi-wiki-skip-subtrees 348 | (org-end-of-subtree))))))))) 349 | (match :initform #'identity) 350 | (fuzzy-match :initform nil) 351 | (multimatch :initform nil) 352 | (nohighlight :initform t) 353 | (volatile :initform t) 354 | (keymap :initform 'helm-org-multi-wiki-map) 355 | (action :initform (or helm-org-multi-wiki-actions 356 | helm-org-ql-actions)))) 357 | 358 | (defclass helm-org-multi-wiki-source-buffers (helm-source-sync) 359 | ((candidates :initform (lambda () 360 | (-map (lambda (buf) 361 | (cons (buffer-name buf) buf)) 362 | helm-org-multi-wiki-buffers))) 363 | ;; This does not restore the narrowing state, nor does it allow customization. 364 | ;; Maybe work on this later? 365 | (persistent-action :initform (lambda (buf) 366 | (switch-to-buffer buf) 367 | (widen) 368 | (goto-char (point-min)) 369 | (when (re-search-forward org-heading-regexp nil t) 370 | (org-show-entry)))) 371 | (coerce :initform (lambda (buf) 372 | (with-current-buffer buf 373 | (org-multi-wiki-run-mode-hooks)) 374 | buf)) 375 | (action :initform 'helm-org-multi-wiki-file-actions))) 376 | 377 | (cl-defun helm-org-multi-wiki-make-dummy-source (namespaces &key 378 | first 379 | action) 380 | "Create a dummy helm source. 381 | 382 | NAMESPACES is a list of symbols. 383 | 384 | FIRST is the target namespace of the first action, as in 385 | `helm-org-multi-wiki' function. 386 | 387 | If ACTION is given, it is used to handle the input. It should be 388 | a function that takes two arguments: a string and a namespace." 389 | (helm-build-dummy-source "New entry" 390 | :keymap helm-org-multi-wiki-dummy-source-map 391 | :action 392 | (mapcar `(lambda (namespace) 393 | (cons (format "Create a new entry in %s%s" 394 | namespace 395 | (if (equal namespace org-multi-wiki-current-namespace) 396 | " (current)" 397 | "")) 398 | (-partial (or (quote ,action) #'helm-org-multi-wiki--create-entry) 399 | namespace))) 400 | (if (and first (> (length namespaces) 1)) 401 | (cons first (-remove-item first namespaces)) 402 | namespaces)))) 403 | 404 | (defconst helm-org-multi-wiki-prompt 405 | "Query (boolean AND): ") 406 | 407 | ;;;###autoload 408 | (cl-defun helm-org-multi-wiki (&optional namespaces &key first) 409 | "Visit an entry or create a new entry. 410 | 411 | NAMESPACES are are a list of namespaces. 412 | It can be a list of symbols or a symbol. 413 | 414 | If FIRST is given, it will be the default namespace in which an 415 | entry is created." 416 | (interactive (list current-prefix-arg)) 417 | ;; Based on the implementation of helm-org-ql. 418 | (if (equal namespaces '(4)) 419 | (helm-org-multi-wiki-namespace 420 | :action 421 | (list (cons "Run helm-org-mult-wiki on selected namespaces" 422 | (lambda (_) 423 | (helm-org-multi-wiki (helm-marked-candidates)))))) 424 | (let* ((namespaces (helm-org-multi-wiki--normalize-namespaces namespaces)) 425 | (helm-input-idle-delay helm-org-ql-input-idle-delay) 426 | (namespace-str (mapconcat #'symbol-name namespaces ",")) 427 | (default-namespace (or helm-org-multi-wiki-default-namespace 428 | org-multi-wiki-current-namespace))) 429 | (helm-org-multi-wiki-with-namespace-buffers namespaces 430 | (helm :prompt helm-org-multi-wiki-prompt 431 | :buffer "*helm org multi wiki*" 432 | :sources 433 | (delq nil 434 | (list (when helm-org-multi-wiki-show-files 435 | (helm-make-source (format "Wiki files in %s" namespace-str) 436 | 'helm-org-multi-wiki-source-buffers)) 437 | (helm-make-source (format "Wiki (%s)" namespace-str) 438 | 'helm-org-multi-wiki-source) 439 | (helm-org-multi-wiki-make-dummy-source 440 | namespaces 441 | :first (or first 442 | (if (memq default-namespace namespaces) 443 | default-namespace 444 | (car namespaces))))))))))) 445 | 446 | ;;;###autoload 447 | (defun helm-org-multi-wiki-all () 448 | "Run `helm-org-multi-wiki' on all configured directories." 449 | (interactive) 450 | (helm-org-multi-wiki (mapcar #'car org-multi-wiki-namespace-list) 451 | :first (or helm-org-multi-wiki-default-namespace 452 | org-multi-wiki-current-namespace))) 453 | 454 | ;;;###autoload 455 | (cl-defun helm-org-multi-wiki-insert-link (&key first) 456 | "Insert a link or converts the region to a link. 457 | 458 | FIRST is the default namespace when you create a non-existent 459 | entry." 460 | (interactive) 461 | (unless (derived-mode-p 'org-mode) 462 | (user-error "Not in org-mode")) 463 | ;; TODO: Add support for regions 464 | (let* ((namespaces (mapcar #'car org-multi-wiki-namespace-list)) 465 | (helm-input-idle-delay helm-org-ql-input-idle-delay) 466 | (namespace-str (mapconcat #'symbol-name namespaces ",")) 467 | (context (helm-org-multi-wiki--link-context))) 468 | (helm-org-multi-wiki-with-namespace-buffers namespaces 469 | (helm :prompt (cl-case (car context) 470 | (region 471 | "Replace the region with a link: ") 472 | (link 473 | "Replace the link: ") 474 | (verbatim 475 | "Replace the text with a link: ") 476 | (_ 477 | "Insert a link: ")) 478 | :buffer "*helm org multi wiki*" 479 | :input (-let (((&plist :text :uri) (cdr context))) 480 | (or text uri)) 481 | :sources 482 | (list (when helm-org-multi-wiki-show-files 483 | (helm-make-source (format "Wiki files in %s" namespace-str) 484 | 'helm-org-multi-wiki-source-buffers 485 | :action #'helm-org-multi-wiki-file-link-insert-action)) 486 | (helm-make-source (format "Wiki (%s)" namespace-str) 487 | 'helm-org-multi-wiki-source 488 | :action helm-org-multi-wiki-insert-link-actions) 489 | (helm-org-multi-wiki-make-dummy-source namespaces 490 | :action #'helm-org-multi-wiki--insert-new-entry-link 491 | :first (or first 492 | helm-org-multi-wiki-default-namespace 493 | org-multi-wiki-current-namespace))))))) 494 | 495 | (provide 'helm-org-multi-wiki) 496 | ;;; helm-org-multi-wiki.el ends here 497 | -------------------------------------------------------------------------------- /notes.org: -------------------------------------------------------------------------------- 1 | #+title: Notes for developing org-multi-wiki 2 | * DONE [#A] Enable org-multi-wiki-mode minor mode in wiki buffers 3 | CLOSED: [2020-03-13 Fri 02:09] 4 | :PROPERTIES: 5 | :CREATED_TIME: [2020-02-28 Fri 09:08] 6 | :END: 7 | This is necessary for implementing some other features. 8 | * DONE [#A] Linking to an entry in the same wiki 9 | CLOSED: [2020-03-13 Fri 02:09] 10 | :PROPERTIES: 11 | :CREATED_TIME: [2020-02-17 Mon 04:13] 12 | :END: 13 | When it inserts a link to an entry in the same wiki, it should omit the directory ID. 14 | 15 | Perhaps it can be implemented by advising =org-link-make-string= to modify the link. 16 | This is a hack, but this seems to be the only way according to the current implementation of =org-insert-link=. 17 | I will a custom variable to enable this feature. 18 | 19 | The entry format should be like ~wiki::TITLE~. 20 | * DONE [#A] Wording: Use "namespace" instead of "directory id" 21 | CLOSED: [2020-03-15 Sun 15:48] 22 | * [#A] Namespace dashboard 23 | :PROPERTIES: 24 | :CREATED_TIME: [2020-03-18 Wed 01:03] 25 | :END: 26 | How should I implement one? 27 | * TODO [#B] Visualisation: Namespace overview 28 | :PROPERTIES: 29 | :CREATED_TIME: [2020-03-14 Sat 17:54] 30 | :END: 31 | * TODO [#B] Linking to non-org files in wiki directories 32 | :PROPERTIES: 33 | :CREATED_TIME: [2020-02-17 Mon 04:13] 34 | :END: 35 | It should be able to store a link to a non-org file in a wiki directory. 36 | Inline images inserted via the link scheme should be displayed properly. 37 | * DONE Recursive file search 38 | CLOSED: [2020-02-28 Fri 02:03] 39 | :PROPERTIES: 40 | :CREATED_TIME: [2020-02-17 Mon 04:16] 41 | :END: 42 | * TODO [#B] Visualisation: Namespace dependencies 43 | :PROPERTIES: 44 | :CREATED_TIME: [2020-03-14 Sat 17:54] 45 | :END: 46 | * TODO Prohibit refiling from wiki 47 | :PROPERTIES: 48 | :CREATED_TIME: [2020-02-20 Thu 03:19] 49 | :END: 50 | It may be important to prohibit refiling entries that are already in a wiki. 51 | Refiling wiki entries to other places can cause deadlinks, which makes the wiki much useless. 52 | It is possible to implement this as a function advice, but a better solution would be to define a minor mode =org-multi-wiki-mode= which can be enabled inside =org-mode=. 53 | -------------------------------------------------------------------------------- /org-multi-wiki-tests.el: -------------------------------------------------------------------------------- 1 | ;;; -*- lexical-binding: t -*- 2 | 3 | (require 'buttercup) 4 | (require 'org-multi-wiki) 5 | 6 | (describe "Defaults" 7 | (describe "org-multi-wiki-escape-file-name-camelcase-1" 8 | (cl-flet ((escape-fn (heading) (org-multi-wiki-escape-file-name-camelcase-1 heading))) 9 | 10 | (it "does not camel case a single word" 11 | (let ((result (escape-fn "hello123"))) 12 | (expect result :to-equal "hello123"))) 13 | 14 | (it "retain dots" 15 | (let ((result (escape-fn "github.com"))) 16 | (expect result :to-equal "github.com"))) 17 | 18 | (it "retain CJK characters" 19 | (let ((result (escape-fn "あいうえお雙拼輸入方案"))) 20 | (expect result :to-equal "あいうえお雙拼輸入方案"))) 21 | 22 | (it "join multiple words and upcase initials" 23 | (let ((result (escape-fn "hello world"))) 24 | (expect result :to-equal "HelloWorld")) 25 | (let ((result (escape-fn "hello WORLD"))) 26 | (expect result :to-equal "HelloWORLD")) 27 | (let ((result (escape-fn "hello WORLD1"))) 28 | (expect result :to-equal "HelloWORLD1")) 29 | (let ((result (escape-fn "hello wOrld"))) 30 | (expect result :to-equal "HelloWOrld"))) 31 | 32 | (it "Don't treat hyphens and underscores as word separators" 33 | (let ((result (escape-fn "org-refile"))) 34 | (expect result :to-equal "org-refile")) 35 | (let ((result (escape-fn "foo_id_1"))) 36 | (expect result :to-equal "foo_id_1"))) 37 | 38 | (it "eliminates words like a, an, and the" 39 | (let ((result (escape-fn "This is a pen"))) 40 | (expect result :to-equal "ThisIsPen")) 41 | (let ((result (escape-fn "This is the knife"))) 42 | (expect result :to-equal "ThisIsKnife"))) 43 | 44 | (it "eliminates most symbols not specified in the above" 45 | (let ((result (escape-fn "123@#!([]<\\|hello"))) 46 | (expect result :to-equal "123hello"))) 47 | 48 | (it "Don't eliminate slash" 49 | (let ((result (escape-fn "hello/john connor"))) 50 | (expect result :to-equal "hello/JohnConnor"))))) 51 | 52 | (describe "org-multi-wiki-default-custom-id-escape-fn" 53 | (cl-flet ((escape-fn (heading) (org-multi-wiki-default-custom-id-escape-fn heading))) 54 | 55 | (it "Converts to lower case" 56 | (let ((result (escape-fn "This is an apple"))) 57 | (expect result :to-equal "this-is-an-apple"))) 58 | 59 | (it "Splits words by spaces, hyphens, and underscores" 60 | (let ((result (escape-fn "org-multi-wiki is an Emacs Lisp package"))) 61 | (expect result :to-equal "org-multi-wiki-is-an-emacs-lisp-package")) 62 | (let ((result (escape-fn "Elixir uses under_scores in symbols"))) 63 | (expect result :to-equal "elixir-uses-under-scores-in-symbols"))) 64 | 65 | (it "Eliminates other symbols" 66 | (let ((result (escape-fn "I was surprised to see it coming!"))) 67 | (expect result :to-equal "i-was-surprised-to-see-it-coming"))))) 68 | 69 | (describe "org-multi-wiki-default-entry-template-fn" 70 | (cl-flet ((template (heading) (org-multi-wiki-default-entry-template-fn heading))) 71 | 72 | (it "generates a top-level entry" 73 | (let ((result (template "Hello"))) 74 | (expect result :to-equal "* Hello\n")))))) 75 | 76 | (describe "org-multi-wiki--strip-org-extension" 77 | (it "strips .org" 78 | (expect (org-multi-wiki--strip-org-extension "sample.org") 79 | :to-equal "sample")) 80 | (it "strips .org.gpg" 81 | (expect (org-multi-wiki--strip-org-extension "sample.org.gpg") 82 | :to-equal "sample")) 83 | (it "retains the original" 84 | (expect (org-multi-wiki--strip-org-extension "sample.txt") 85 | :to-equal "sample.txt") 86 | (expect (org-multi-wiki--strip-org-extension "hello.org.nonorg") 87 | :to-equal "hello.org.nonorg"))) 88 | -------------------------------------------------------------------------------- /org-multi-wiki.el: -------------------------------------------------------------------------------- 1 | ;;; org-multi-wiki.el --- Multiple wikis based on Org mode -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2020 Akira Komamura 4 | 5 | ;; Author: Akira Komamura 6 | ;; Version: 0.4.3 7 | ;; Package-Requires: ((emacs "26.1") (dash "2.12") (s "1.12") (org-ql "0.5") (org "9.3")) 8 | ;; Keywords: org outlines files 9 | ;; URL: https://github.com/akirak/org-multi-wiki 10 | 11 | ;; This file is not part of GNU Emacs. 12 | 13 | ;;; License: 14 | 15 | ;; This program is free software: you can redistribute it and/or modify 16 | ;; it under the terms of the GNU General Public License as published by 17 | ;; the Free Software Foundation, either version 3 of the License, or 18 | ;; (at your option) any later version. 19 | ;; 20 | ;; This program is distributed in the hope that it will be useful, 21 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 22 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 23 | ;; GNU General Public License for more details. 24 | ;; 25 | ;; You should have received a copy of the GNU General Public License 26 | ;; along with this program. If not, see . 27 | 28 | ;;; Commentary: 29 | 30 | ;; This library provides an infrastructure for multiple wikis on a 31 | ;; single machine, based on org-mode. 32 | 33 | ;; For configuration, see README. 34 | 35 | ;; For practical use, see helm-org-multi-wiki.el. 36 | 37 | ;;; Code: 38 | 39 | (require 'subr-x) 40 | (require 'dash) 41 | (require 's) 42 | (require 'org) 43 | (require 'ol) 44 | 45 | (declare-function org-ql-select "ext:org-ql-select") 46 | 47 | (defvar recentf-exclude) 48 | 49 | (defgroup org-multi-wiki nil 50 | "Multiple wikis based on org-mode." 51 | :group 'org) 52 | 53 | ;;;; Custom variables 54 | (defvar org-multi-wiki-file-regexp) 55 | 56 | (defun org-multi-wiki--extensions-to-regexp (extensions) 57 | "Produce a regular expression for a list of file EXTENSIONS." 58 | (concat "\\`[^.].*\\(?:" 59 | (mapconcat (lambda (ext) 60 | (concat "\\(?:" 61 | (regexp-quote ext) 62 | "\\)")) 63 | extensions 64 | "\\|") 65 | "\\)\\'")) 66 | 67 | (defcustom org-multi-wiki-file-extensions '(".org" ".org.gpg") 68 | "List of file extensions for wiki entries. 69 | 70 | The first one is used to create a new file by default." 71 | :type '(repeat string) 72 | :set (lambda (sym value) 73 | (set sym value) 74 | (setq org-multi-wiki-file-regexp 75 | (org-multi-wiki--extensions-to-regexp value))) 76 | :group 'org-multi-wiki) 77 | 78 | (defvar org-multi-wiki-recentf-regexp nil) 79 | 80 | (defun org-multi-wiki--recentf-regexp (namespace-list) 81 | "Compile `org-multi-wiki-recentf-regexp' for later use. 82 | 83 | NAMESPACE-LIST should be the value of the namespace list." 84 | (when namespace-list 85 | (let ((dirs (->> namespace-list 86 | (--map (let ((dir (file-name-as-directory 87 | (expand-file-name (nth 1 it))))) 88 | (->> (list dir (ignore-errors 89 | (file-truename dir))) 90 | (-non-nil) 91 | (-uniq)))) 92 | (-flatten-n 1)))) 93 | (rx-to-string `(and bol 94 | (or ,@dirs) 95 | (+ anything) 96 | (or ,@org-multi-wiki-file-extensions) 97 | eol))))) 98 | 99 | (defcustom org-multi-wiki-namespace-list 100 | nil 101 | "List of namespace configurations for wikis. 102 | 103 | Each entry in this variable should be a list containing the 104 | following items, in that order: 105 | 106 | - A symbol to uniquely identify the directory. 107 | - A file path to the directory containing Org files. 108 | - A plist to set directory-specific options. 109 | 110 | The plist can contain the following keys which correspond certain 111 | custom variables for the global setting: 112 | 113 | - `:top-level-link-fragments'" 114 | :type '(repeat (list (symbol :tag "Namespace") 115 | (directory :tag "Directory") 116 | (plist :inline t :tag "Options" 117 | :options 118 | (((const :doc "Generate a link fragment to each top-level heading." 119 | :top-level-link-fragments) 120 | (boolean)) 121 | ((const :doc "Recursively search files in subdirectories" 122 | :recursive) 123 | (boolean)))))) 124 | :set (lambda (sym value) 125 | (set sym value) 126 | (setq org-multi-wiki-recentf-regexp 127 | (org-multi-wiki--recentf-regexp value))) 128 | :group 'org-multi-wiki) 129 | 130 | (define-obsolete-variable-alias 'org-multi-wiki-directories 131 | org-multi-wiki-namespace-list "0.3") 132 | 133 | (defcustom org-multi-wiki-default-namespace 134 | (caar org-multi-wiki-namespace-list) 135 | "Default namespace of wikis. 136 | 137 | This should be the first element of one of the entries in 138 | `org-multi-wiki-namespace-list'." 139 | :type 'symbol 140 | :group 'org-multi-wiki) 141 | 142 | (defcustom org-multi-wiki-recentf-exclude nil 143 | "Whether to exclude wiki files from recent files." 144 | :type 'boolean 145 | :set (lambda (symbol value) 146 | (set symbol value) 147 | (require 'recentf) 148 | (if value 149 | (add-to-list 'recentf-exclude #'org-multi-wiki-recentf-file-p t) 150 | (delq #'org-multi-wiki-recentf-file-p recentf-exclude)))) 151 | 152 | (defcustom org-multi-wiki-escape-file-name-fn 153 | #'org-multi-wiki-escape-file-name-camelcase-1 154 | "Function used to generated an escaped file name from a heading." 155 | :type 'function 156 | :group 'org-multi-wiki) 157 | 158 | (defcustom org-multi-wiki-entry-template-fn 159 | #'org-multi-wiki-default-entry-template-fn 160 | "Function to create an initial Org entry from a heading." 161 | :type 'function 162 | :group 'org-multi-wiki) 163 | 164 | (defcustom org-multi-wiki-display-buffer-fn 165 | #'pop-to-buffer 166 | "Function used to display Org buffers." 167 | :type 'function 168 | :group 'org-multi-wiki) 169 | 170 | (defcustom org-multi-wiki-ignore-missing-directories nil 171 | "Whether to prevent an error when a directory is missing. 172 | 173 | When non-nil, return an empty result from 174 | `org-multi-wiki-entry-files' when the directory does not exist." 175 | :type 'boolean 176 | :group 'org-multi-wiki) 177 | 178 | (defcustom org-multi-wiki-filename-removed-words 179 | '("a" "an" "the") 180 | "List of words that should be removed from file names." 181 | :type '(repeat string) 182 | :group 'org-multi-wiki) 183 | 184 | (defcustom org-multi-wiki-want-custom-id nil 185 | "Whether to prompt for a CUSTOM_ID property. 186 | 187 | If this variable is non-nil, non-top-level headings in a wiki 188 | entry will always be linked with a CUSTOM_ID." 189 | :type 'boolean 190 | :group 'org-multi-wiki) 191 | 192 | (defcustom org-multi-wiki-custom-id-escape-fn 193 | #'org-multi-wiki-default-custom-id-escape-fn 194 | "Function used to escape CUSTOM_ID properties. 195 | 196 | The function takes a heading as the argument." 197 | :type 'function 198 | :group 'org-multi-wiki) 199 | 200 | (defcustom org-multi-wiki-top-level-link-fragments nil 201 | "Whether to add an ID/headline fragment to a link to each top level heading." 202 | :type 'boolean 203 | :group 'org-multi-wiki) 204 | 205 | (defcustom org-multi-wiki-allow-omit-namespace t 206 | "Whether to omit the namespace ID in a link to the same namespace." 207 | :type 'boolean 208 | :group 'org-multi-wiki) 209 | 210 | (defcustom org-multi-wiki-rename-buffer t 211 | "Whether to rename Org buffers to represent the directory. 212 | 213 | When this variable is non-nil, Org buffers opened this package 214 | are renamed so that they contain their directory IDs. 215 | 216 | This is a hack for `helm-org-ql'. 217 | 218 | This setting does not affect buffers that are already open" 219 | :type 'boolean 220 | :group 'org-multi-wiki) 221 | 222 | (defcustom org-multi-wiki-rg-executable "rg" 223 | "Executable name of ripgrep." 224 | :type 'filename 225 | :group 'org-multi-wiki) 226 | 227 | (defcustom org-multi-wiki-rg-args '("--color=never" "--files") 228 | "Command line arguments passed to rg." 229 | :type '(repeat string) 230 | :group 'org-multi-wiki) 231 | 232 | (defcustom org-multi-wiki-buffer-name-fn 233 | #'org-multi-wiki-buffer-name-1 234 | "Function to determine the names of Org buffers. 235 | 236 | The function takes a plist as arguments. 237 | See `org-multi-wiki-buffer-name-1' for an example." 238 | :type 'function 239 | :group 'org-multi-wiki) 240 | 241 | (defcustom org-multi-wiki-removal-block-functions 242 | '(org-multi-wiki-entry-file-p) 243 | "Block removal of a subtree if any of these functions returns non-nil. 244 | 245 | This is curently effective in 246 | `org-multi-wiki-create-entry-from-subtree'. 247 | 248 | These functions take no argument and should return non-nil if the 249 | user must not muve the subtree at point to another file." 250 | :type '(repeat function) 251 | :group 'org-multi-wiki) 252 | 253 | ;;;; Other variables 254 | (defvar org-multi-wiki-current-namespace org-multi-wiki-default-namespace) 255 | 256 | (defvar-local org-multi-wiki-mode-hooks-delayed nil 257 | "Whether `run-mode-hooks' has been delayed in the buffer.") 258 | 259 | (defvar org-multi-wiki-gpg-skip-file-list nil) 260 | (defvar org-multi-wiki-gpg-skip-namespace-list nil) 261 | (defvar org-multi-wiki-gpg-skip-globally nil) 262 | 263 | ;;;; Macros 264 | (defmacro org-multi-wiki--def-option (key) 265 | "Define a function to retrieve KEY option." 266 | (let ((func (intern (format "org-multi-wiki--%s" key))) 267 | (plist-key (intern (concat ":" key))) 268 | (default-var (intern (concat "org-multi-wiki-" key)))) 269 | `(defun ,func (namespace) 270 | ,(format "Retrieve the value of %s for NAMESPACE." key) 271 | (if-let (entry (assoc namespace org-multi-wiki-namespace-list)) 272 | (let ((plist (cddr entry))) 273 | (or (plist-get plist ,plist-key) 274 | (symbol-value ,default-var))) 275 | (user-error "No entry for %s in org-multi-wiki-namespace-list" 276 | namespace))))) 277 | 278 | (org-multi-wiki--def-option "top-level-link-fragments") 279 | 280 | ;;;; Default functions 281 | (defun org-multi-wiki-escape-file-name-camelcase-1 (heading) 282 | "Escape HEADING suitable for use in file name." 283 | (cl-labels ((filename-escape 284 | (str) 285 | (s-replace-regexp (rx (not (any alnum "-._" nonascii))) "" str))) 286 | (-let* (((_ dir name) (s-match (rx bol 287 | (group (* (*? anything) "/")) 288 | (group (+ anything)) 289 | eol) 290 | heading)) 291 | (words (split-string name (rx (any space))))) 292 | (concat dir 293 | (if (= 1 (length words)) 294 | (filename-escape (car words)) 295 | (->> words 296 | (-filter #'org-multi-wiki--meaningful-word-p) 297 | (-map #'filename-escape) 298 | (-map #'upcase-initials) 299 | (string-join))))))) 300 | 301 | (defun org-multi-wiki--meaningful-word-p (word) 302 | "Check if WORD is a meaningful word. 303 | 304 | This function returns nil if the word should be removed from a 305 | file name." 306 | (not (cl-member word org-multi-wiki-filename-removed-words 307 | :test #'string-equal))) 308 | 309 | (defun org-multi-wiki-default-entry-template-fn (heading) 310 | "Generate an Org entry from HEADING." 311 | (concat "* " heading "\n")) 312 | 313 | (defun org-multi-wiki-default-custom-id-escape-fn (heading) 314 | "Escape HEADING for a CUSTOM_ID property." 315 | (--> (split-string heading (rx (any "-_" space))) 316 | (-map (lambda (str) 317 | (s-replace-regexp (rx (not (any alnum))) "" str)) 318 | it) 319 | (-map #'downcase it) 320 | (string-join it "-"))) 321 | 322 | ;;;; Modes 323 | ;;;###autoload 324 | (define-minor-mode org-multi-wiki-global-mode nil 325 | nil nil nil 326 | :global t 327 | :after-hook 328 | (cond 329 | (org-multi-wiki-global-mode 330 | (add-hook 'org-mode #'org-multi-wiki-check-buffer) 331 | (advice-add 'org-link-escape 332 | :filter-return #'org-multi-wiki-strip-namespace) 333 | ;; Run mode hooks when `org-show-entry' is called. 334 | ;; This is useful when an entry is visited by `helm-org-ql'. 335 | (advice-add 'org-show-entry :before #'org-multi-wiki-run-mode-hooks) 336 | (org-link-set-parameters "wiki" :follow #'org-multi-wiki-follow-link 337 | :store #'org-multi-wiki-store-link 338 | :complete #'org-multi-wiki-complete-link)) 339 | (t 340 | (remove-hook 'org-mode #'org-multi-wiki-check-buffer) 341 | (advice-remove 'org-link-escape #'org-multi-wiki-strip-namespace) 342 | (advice-remove 'org-show-entry #'org-multi-wiki-run-mode-hooks) 343 | (cl-delete (assoc "wiki" org-link-parameters) org-link-parameters)))) 344 | 345 | (define-minor-mode org-multi-wiki-mode 346 | "Minor mode that should be activated in all wiki buffers." 347 | nil nil nil) 348 | 349 | ;;;; Configuration helpers 350 | ;;;###autoload 351 | (cl-defun org-multi-wiki-add-namespaces (namespaces) 352 | "Add entries to `org-multi-wiki-namespace-list'. 353 | 354 | This is a convenient function for adding an entry to the namespace list. 355 | 356 | NAMESPACES should be a list of entries to add to the 357 | variable. There won't be duplicate namespaces, and hooks for the 358 | variable is run if necessary." 359 | (dolist (entry namespaces) 360 | (let ((cell (assoc (car entry) org-multi-wiki-namespace-list))) 361 | (funcall (or (get 'org-multi-wiki-namespace-list 'custom-set) #'set) 362 | 'org-multi-wiki-namespace-list 363 | (if cell 364 | (progn 365 | (setcdr cell (cdr entry)) 366 | org-multi-wiki-namespace-list) 367 | (append org-multi-wiki-namespace-list 368 | (list entry))))))) 369 | 370 | ;;;; File and directory infrastructure 371 | (defun org-multi-wiki-check-buffer () 372 | "Check if the current buffer is an wiki entry." 373 | (when (ignore-errors 374 | (org-multi-wiki-entry-file-p)) 375 | (org-multi-wiki-mode 1))) 376 | 377 | (defun org-multi-wiki-directory (&optional namespace) 378 | "Get the root directory of NAMESPACE." 379 | (let ((namespace (or namespace org-multi-wiki-current-namespace))) 380 | (or (car-safe (alist-get namespace org-multi-wiki-namespace-list)) 381 | (error "No entry exists for %s in org-multi-wiki-namespace-list" 382 | namespace)))) 383 | 384 | (defun org-multi-wiki-select-namespace (&optional prompt) 385 | "Select a wiki id using `completing-read', with an optional PROMPT." 386 | (intern (completing-read (or prompt (format "Wiki [current %s]: " 387 | org-multi-wiki-current-namespace)) 388 | (mapcar #'car org-multi-wiki-namespace-list) 389 | nil t nil nil org-multi-wiki-current-namespace))) 390 | 391 | ;;;###autoload 392 | (defun org-multi-wiki-entry-file-p (&optional file) 393 | "Check if FILE is a wiki entry. 394 | 395 | If the file is a wiki entry, this functions returns a plist. 396 | 397 | If FILE is omitted, the current buffer is assumed." 398 | (let* ((file (or file (buffer-file-name (or (org-base-buffer (current-buffer)) 399 | (current-buffer))))) 400 | (directory (file-name-directory file)) 401 | root-directory sans-extension namespace) 402 | (and (-any (lambda (extension) 403 | (when (string-suffix-p extension file) 404 | (setq sans-extension (string-remove-suffix extension file)))) 405 | org-multi-wiki-file-extensions) 406 | (-any (lambda (entry) 407 | (let ((dir (file-name-as-directory (nth 1 entry)))) 408 | (when (or (file-equal-p directory dir) 409 | (string-prefix-p (expand-file-name dir) 410 | (expand-file-name directory))) 411 | (setq root-directory dir 412 | namespace (car entry))))) 413 | org-multi-wiki-namespace-list) 414 | (list :file file 415 | :namespace namespace 416 | :basename (file-relative-name (file-truename sans-extension) 417 | (file-truename root-directory)))))) 418 | 419 | ;;;###autoload 420 | (defsubst org-multi-wiki-recentf-file-p (filename) 421 | "Test if FILENAME matches the recentf exclude pattern. 422 | 423 | This is not exactly the same as 424 | `org-multi-wiki-entry-file-p'. This one tries to be faster by 425 | using a precompiled regular expression, at the cost of accuracy." 426 | ;; The regular expression can be nil if the namespace list is empty 427 | (when org-multi-wiki-recentf-regexp 428 | (string-match-p org-multi-wiki-recentf-regexp filename))) 429 | 430 | ;;;###autoload 431 | (defun org-multi-wiki-in-namespace-p (namespace &optional dir) 432 | "Check if a file/directory is in a particular namespace. 433 | 434 | This checks if the directory is in/on a wiki NAMESPACE, which is 435 | a symbol. If the directory is in/on the namespace, this function 436 | returns non-nil. 437 | 438 | By default, the directory is `default-directory', but you can 439 | explicitly give it as DIR." 440 | (let* ((data (or (assoc namespace org-multi-wiki-namespace-list) 441 | (error "Namespace %s is undefined" namespace))) 442 | (root (nth 1 data)) 443 | (recursive (plist-get (-drop 2 data) :recursive)) 444 | (dir (or (and dir (file-name-as-directory dir)) 445 | default-directory))) 446 | (if recursive 447 | (string-prefix-p (file-truename root) (file-truename dir)) 448 | (file-equal-p dir root)))) 449 | 450 | (defun org-multi-wiki--current-namespace () 451 | "Return the namespace of the current buffer." 452 | (plist-get (org-multi-wiki-entry-file-p) :namespace)) 453 | 454 | (defun org-multi-wiki--plist-get (prop &optional namespace) 455 | "Select PROP from the properties of NAMESPACE." 456 | (let* ((namespace (or namespace org-multi-wiki-current-namespace)) 457 | (plist (cdr-safe (alist-get namespace org-multi-wiki-namespace-list)))) 458 | (plist-get plist prop))) 459 | 460 | ;;;###autoload 461 | (cl-defun org-multi-wiki-entry-files (&optional namespace &key as-buffers) 462 | "Get a list of Org files in a namespace. 463 | 464 | If NAMESPACE is omitted, the current namespace is used, as in 465 | `org-multi-wiki-directory'. 466 | 467 | If AS-BUFFERS is non-nil, this function returns a list of buffers 468 | instead of file names." 469 | (let* ((namespace (or namespace org-multi-wiki-current-namespace)) 470 | (dir (org-multi-wiki-directory namespace)) 471 | (recursive (org-multi-wiki--plist-get :recursive namespace)) 472 | (files (if recursive 473 | (org-multi-wiki--org-files-recursively dir) 474 | (directory-files dir t org-multi-wiki-file-regexp)))) 475 | (if as-buffers 476 | (->> files 477 | (-map (lambda (file) 478 | (or (find-buffer-visiting file) 479 | (org-multi-wiki--find-file-noselect :namespace namespace 480 | :file file 481 | :dir dir)))) 482 | ;; If there is a file which failed to decrypt, it is nil. 483 | (delq nil)) 484 | files))) 485 | 486 | (cl-defun org-multi-wiki--find-file-noselect (&key file namespace dir) 487 | "Create a new buffer for an Org file. 488 | 489 | FILE is an absolute file to the Org file, and NAMESPACE and DIR 490 | contain the file." 491 | (let ((gpg-p (string-suffix-p ".gpg" file))) 492 | ;; If some files are GPG-encrypted and the 493 | ;; key is temporarily unavailble, the user 494 | ;; may want to read only unencrypted files. 495 | ;; 496 | ;; As a workaround, if there is an error 497 | ;; while reading a file ending with .gpg, 498 | ;; this function assumes that it is a 499 | ;; decryption issue and skips the following 500 | ;; decryption. 501 | (unless (and gpg-p 502 | (or org-multi-wiki-gpg-skip-globally 503 | (memq namespace org-multi-wiki-gpg-skip-namespace-list) 504 | (member file org-multi-wiki-gpg-skip-file-list))) 505 | (condition-case nil 506 | (let* ((default-directory (file-name-directory file)) 507 | (buf (create-file-buffer file))) 508 | ;; Based on the implementation by @kungsgeten 509 | ;; for faster loading of many Org files. 510 | ;; https://github.com/alphapapa/org-ql/issues/88#issuecomment-570568341 511 | (with-current-buffer buf 512 | (insert-file-contents file) 513 | (setq buffer-file-name file) 514 | (when org-multi-wiki-rename-buffer 515 | (rename-buffer 516 | (funcall org-multi-wiki-buffer-name-fn 517 | :namespace namespace :file file :dir dir) 518 | t)) 519 | (set-buffer-modified-p nil) 520 | ;; Use delay-mode-hooks for faster loading. 521 | (delay-mode-hooks (set-auto-mode)) 522 | (setq org-multi-wiki-mode-hooks-delayed t)) 523 | buf) 524 | (error (progn 525 | (when gpg-p 526 | (pcase (read-char-choice 527 | "Skip the following decryption on [f]ile, [n]amespace, [a]ll: " 528 | (string-to-list "fna")) 529 | (?f (push file org-multi-wiki-gpg-skip-file-list)) 530 | (?n (push namespace org-multi-wiki-gpg-skip-namespace-list)) 531 | (?a (setq org-multi-wiki-gpg-skip-globally t)))) 532 | nil)))))) 533 | 534 | (defun org-multi-wiki-run-mode-hooks () 535 | "Run mode hooks delayed by org-multi-wiki." 536 | (when org-multi-wiki-mode-hooks-delayed 537 | (run-mode-hooks) 538 | (setq org-multi-wiki-mode-hooks-delayed nil))) 539 | 540 | (cl-defun org-multi-wiki-buffer-name-1 (&key namespace file dir) 541 | "Return a buffer name suitable for Wiki. 542 | 543 | NAMESPACE is the name space of the wiki, FILE is the file name, 544 | and DIR is the root directory of the namespace." 545 | (format "%s:%s" namespace (file-relative-name file dir))) 546 | 547 | (defun org-multi-wiki--org-files-recursively (dir) 548 | "Get a list of Org files in DIR recursively." 549 | (let ((default-directory dir)) 550 | (mapcar (lambda (fpath) (expand-file-name fpath dir)) 551 | (apply #'process-lines 552 | org-multi-wiki-rg-executable 553 | "-g" (format "*{%s}" 554 | (string-join org-multi-wiki-file-extensions 555 | ",")) 556 | org-multi-wiki-rg-args)))) 557 | 558 | (defun org-multi-wiki-expand-org-file-names (directory basename) 559 | "Return a list of possible Org file names in DIRECTORY with BASENAME." 560 | (-map (lambda (extension) 561 | (expand-file-name (concat basename extension) directory)) 562 | org-multi-wiki-file-extensions)) 563 | 564 | (cl-defun org-multi-wiki-link-file-name (file &key namespace dir) 565 | "Return a file name in an Org link. 566 | 567 | FILE is an absolute file name to an Org file. 568 | 569 | Either NAMESPACE or DIR to the wiki should be specified." 570 | (let ((dir (or dir (org-multi-wiki-directory namespace))) 571 | (extension (-find (lambda (extension) 572 | (string-suffix-p extension file)) 573 | org-multi-wiki-file-extensions))) 574 | (unless extension 575 | (error "No matching extension in `org-multi-wiki-file-extensions'")) 576 | (->> (file-relative-name file dir) 577 | (string-remove-suffix extension)))) 578 | 579 | (defun org-multi-wiki--find-heading (heading dir) 580 | "Find a file of HEADING in DIR." 581 | (let* ((escaped-filenames (org-multi-wiki-expand-org-file-names 582 | dir (funcall org-multi-wiki-escape-file-name-fn heading))) 583 | (filenames (append (org-multi-wiki-expand-org-file-names 584 | dir heading) 585 | escaped-filenames))) 586 | (or (cl-find-if #'file-exists-p filenames) 587 | (car escaped-filenames)))) 588 | 589 | (cl-defun org-multi-wiki--setup-new-buffer (buf namespace fpath dir) 590 | "Set up a buffer for a new wiki entry. 591 | 592 | See `org-multi-wiki-visit-entry' for BUF, NAMESPACE, FPATH, and DIR." 593 | (when org-multi-wiki-rename-buffer 594 | (with-current-buffer buf 595 | (rename-buffer (funcall org-multi-wiki-buffer-name-fn 596 | :namespace namespace :file fpath :dir dir) 597 | t)))) 598 | 599 | ;;;; Custom link type 600 | ;;;###autoload 601 | (defun org-multi-wiki-follow-link (link) 602 | "Follow a wiki LINK." 603 | (when (string-match (rx bol (group-n 1 (* (any alnum "-"))) 604 | ":" (group-n 2 (+? anything)) 605 | (optional "::" 606 | (or (and "#" (group-n 3 (+ anything))) 607 | (and "*" (group-n 4 (+ anything))))) 608 | eol) 609 | link) 610 | (let* ((id (if (string-empty-p (match-string 1 link)) 611 | (save-match-data 612 | (org-multi-wiki--current-namespace)) 613 | (intern (match-string 1 link)))) 614 | (basename (match-string 2 link)) 615 | (custom-id (match-string 3 link)) 616 | (headline (match-string 4 link)) 617 | (info (assoc id org-multi-wiki-namespace-list #'eq)) 618 | (root (if info 619 | (nth 1 info) 620 | (user-error "Wiki directory for %s is undefined" id))) 621 | (file (or (cl-find-if #'file-exists-p 622 | (org-multi-wiki-expand-org-file-names root basename)) 623 | (cl-find-if #'file-exists-p 624 | (org-multi-wiki-expand-org-file-names 625 | root 626 | (funcall org-multi-wiki-escape-file-name-fn basename)))))) 627 | (cond 628 | (file (find-file file)) 629 | (t (let ((marker (and headline 630 | (car-safe (org-ql-select (org-multi-wiki-entry-files id) 631 | `(and (level 1) 632 | (heading ,headline)) 633 | :action '(point-marker)))))) 634 | (if marker 635 | (org-goto-marker-or-bmk marker) 636 | (org-multi-wiki-visit-entry basename :namespace id))))) 637 | (let ((pos (or (and custom-id 638 | (or (car-safe (org-ql-select (current-buffer) 639 | `(property "CUSTOM_ID" ,custom-id) 640 | :action '(point))) 641 | (user-error "Cannot find an entry with CUSTOM_ID %s" custom-id))) 642 | (and headline 643 | (or (car-safe (org-ql-select (current-buffer) 644 | `(heading ,headline) 645 | :action '(point))) 646 | (user-error "Cannot find an entry with heading %s" headline)))))) 647 | (when pos (goto-char pos)))))) 648 | 649 | ;;;###autoload 650 | (defun org-multi-wiki-store-link () 651 | "Store a link." 652 | (when-let* ((plist (org-multi-wiki--get-link-data nil)) 653 | (link-brackets (org-link-make-string (plist-get plist :link) 654 | (plist-get plist :headline)))) 655 | (org-link-store-props :type "wiki" 656 | ;; :file (plist-get plist :file) 657 | ;; :node headline 658 | :link (plist-get plist :link) 659 | :description (plist-get plist :headline)) 660 | link-brackets)) 661 | 662 | (cl-defun org-multi-wiki--make-link (ns basename 663 | &key 664 | origin-ns 665 | custom-id 666 | level 667 | headline 668 | to-file) 669 | "Create a Org link URI. 670 | 671 | For NS, BASENAME, ORIGIN-NS, CUSTOM-ID, LEVEL, and HEADLINE 672 | See `org-multi-wiki--get-link-data' 673 | 674 | When TO-FILE, it generates a link to the file itself." 675 | (format "wiki:%s:%s%s" 676 | (if (not (and origin-ns 677 | org-multi-wiki-allow-omit-namespace 678 | (eq origin-ns ns))) 679 | (symbol-name ns) 680 | "") 681 | basename 682 | (or (and to-file 683 | "") 684 | (and (not (org-multi-wiki--top-level-link-fragments ns)) 685 | (= level 1) 686 | "") 687 | (and custom-id 688 | (concat "::#" custom-id)) 689 | (concat "::*" headline)))) 690 | 691 | (defun org-multi-wiki--get-link-data (&optional origin-ns) 692 | "Return data needed for generating a link. 693 | 694 | ORIGIN-NS, if specified, is the namespace of the link orientation." 695 | (when (derived-mode-p 'org-mode) 696 | (when-let (plist (org-multi-wiki-entry-file-p)) 697 | (when (org-before-first-heading-p) 698 | (user-error "You cannot store the link of a wiki entry before the first heading")) 699 | (-let* (((level _ _ _ headline _) (org-heading-components)) 700 | (custom-id (or (org-entry-get nil "CUSTOM_ID") 701 | (and org-multi-wiki-want-custom-id 702 | (or (org-multi-wiki--top-level-link-fragments (plist-get plist :namespace)) 703 | (> level 1)) 704 | (let* ((default (->> headline 705 | (org-link-display-format) 706 | (funcall org-multi-wiki-custom-id-escape-fn))) 707 | (custom-id (read-string 708 | (format "CUSTOM_ID for the heading [%s]: " 709 | default) 710 | nil nil default))) 711 | (when custom-id 712 | (org-entry-put nil "CUSTOM_ID" custom-id) 713 | custom-id))))) 714 | (clean-headline (org-link-display-format headline))) 715 | (list :link (org-multi-wiki--make-link (plist-get plist :namespace) 716 | (plist-get plist :basename) 717 | :origin-ns origin-ns 718 | :custom-id custom-id 719 | :headline clean-headline 720 | :level level) 721 | :headline clean-headline))))) 722 | 723 | (defun org-multi-wiki-strip-namespace (link) 724 | "Strip namespace from LINK if possible." 725 | (if (and (string-prefix-p "wiki:" link) 726 | org-multi-wiki-allow-omit-namespace 727 | (string-match (rx bol "wiki:" 728 | (group (*? (not (any ":")))) ":" 729 | (group (+ anything)) eol) 730 | link) 731 | (eq (intern (match-string 1 link)) 732 | (save-match-data 733 | (plist-get (org-multi-wiki-entry-file-p) :namespace)))) 734 | (concat "wiki::" (match-string 2 link)) 735 | link)) 736 | 737 | (defun org-multi-wiki--strip-org-extension (filename) 738 | "Strip .org or .org.gpg from FILENAME." 739 | (save-match-data 740 | (if (string-match (rx (or ".org" ".org.gpg") eos) filename) 741 | (substring filename 0 (car (match-data))) 742 | filename))) 743 | 744 | (defun org-multi-wiki-complete-link () 745 | "Support for the Org link completion mechanism." 746 | (let* ((origin-ns (plist-get (org-multi-wiki-entry-file-p) :namespace)) 747 | (namespace (intern (completing-read "Wiki: " 748 | (->> org-multi-wiki-namespace-list 749 | (-map #'car) 750 | (-map #'symbol-name)) 751 | nil t nil nil origin-ns))) 752 | (files (org-multi-wiki-entry-files namespace)) 753 | (alist (mapcar (lambda (file) 754 | (cons (org-multi-wiki-link-file-name 755 | file :namespace namespace) 756 | file)) 757 | files)) 758 | (inp (completing-read "File or heading: " 759 | (mapcar #'car alist) 760 | nil nil 761 | (when (region-active-p) 762 | (buffer-substring-no-properties 763 | (region-beginning) (region-end))))) 764 | (file (cdr-safe (assoc inp alist)))) 765 | (if file 766 | (plist-get (with-current-buffer 767 | (or (find-buffer-visiting file) 768 | (find-file-noselect file)) 769 | (org-with-wide-buffer 770 | (let* ((heading (completing-read "Heading: " 771 | (org-multi-wiki--toplevel-headings-markers) 772 | nil t)) 773 | (marker (get-char-property 0 'marker heading))) 774 | (goto-char marker) 775 | (org-multi-wiki--get-link-data origin-ns)))) 776 | :link) 777 | (org-multi-wiki--make-link namespace 778 | (org-multi-wiki--strip-org-extension inp) 779 | :origin-ns origin-ns 780 | :to-file t)))) 781 | 782 | (defun org-multi-wiki--toplevel-headings-markers () 783 | "Return the top level headings with their markers." 784 | (let (headings) 785 | (goto-char (point-min)) 786 | (while (re-search-forward (rx bol (+ "*") space) nil t) 787 | (push (propertize (string-trim-right (thing-at-point 'line t)) 788 | 'marker (point-marker)) 789 | headings)) 790 | (nreverse headings))) 791 | 792 | ;;;; Other utility functions 793 | 794 | (defun org-multi-wiki--trim-statistic-cookie (text) 795 | "Eliminate statistic cookie from a heading TEXT." 796 | (save-match-data 797 | (if (string-match (rx (+ space) 798 | "[" (or (and (* (any digit)) "%") 799 | (and (+ (any digit)) "/" 800 | (+ (any digit)))) 801 | "]" 802 | (* space) eol) 803 | text) 804 | (substring text 0 (car (match-data))) 805 | text))) 806 | 807 | (defun org-multi-wiki--cleanup-heading (text) 808 | "Clean up an Org heading TEXT to make it neutral." 809 | (->> text 810 | (org-multi-wiki--trim-statistic-cookie) 811 | (org-link-display-format))) 812 | 813 | ;;;; Commands 814 | 815 | ;;;###autoload 816 | (defun org-multi-wiki-switch (namespace) 817 | "Set the current wiki to NAMESPACE." 818 | (interactive (list (org-multi-wiki-select-namespace))) 819 | (when-let (dir (org-multi-wiki-directory namespace)) 820 | (setq org-multi-wiki-current-namespace namespace) 821 | (message "Set the current wiki to \"%s\" (%s)" namespace 822 | org-multi-wiki-current-namespace))) 823 | 824 | ;;;###autoload 825 | (cl-defun org-multi-wiki-visit-entry (heading &key 826 | namespace 827 | filename) 828 | "Visit an entry of the heading. 829 | 830 | HEADING in the root heading of an Org file to create or look 831 | for. It looks for an existing entry in NAMESPACE or create a new 832 | one if none. A file is determined based on 833 | `org-multi-wiki-escape-file-name-fn', unless you explicitly 834 | specify a FILENAME." 835 | (interactive (let ((namespace (or (and current-prefix-arg 836 | (org-multi-wiki-select-namespace)) 837 | org-multi-wiki-current-namespace 838 | (user-error "No current namespace")))) 839 | (list (completing-read (format "org-multi-wiki [namespace %s]: " 840 | namespace) 841 | (->> (org-multi-wiki-entry-files namespace) 842 | (-map (lambda (file) 843 | (org-multi-wiki-link-file-name 844 | file :namespace namespace))))) 845 | :namespace namespace))) 846 | (let* ((dir (org-multi-wiki-directory namespace)) 847 | (fpath (progn 848 | (unless (and dir (file-directory-p dir)) 849 | (user-error "Wiki directory is nil or missing: %s" dir)) 850 | (if filename 851 | (expand-file-name filename dir) 852 | (org-multi-wiki--find-heading heading dir)))) 853 | (new (not (file-exists-p fpath))) 854 | (existing-buffer (find-buffer-visiting fpath)) 855 | ;; Set default-directory to allow directory-specific templates 856 | (default-directory dir) 857 | (buf (or existing-buffer 858 | (when new 859 | (let ((parent (file-name-directory fpath))) 860 | (unless (file-directory-p parent) 861 | (make-directory parent t))) 862 | (with-current-buffer (create-file-buffer fpath) 863 | (setq buffer-file-name fpath) 864 | (insert (funcall org-multi-wiki-entry-template-fn heading)) 865 | (set-auto-mode) 866 | (current-buffer))) 867 | (find-file-noselect fpath)))) 868 | (unless existing-buffer 869 | (org-multi-wiki--setup-new-buffer buf namespace fpath dir)) 870 | (with-current-buffer buf 871 | (org-multi-wiki-run-mode-hooks)) 872 | (funcall org-multi-wiki-display-buffer-fn buf))) 873 | 874 | (defsubst org-multi-wiki--removal-blocked-p () 875 | "Return non-nil if the user must not remove the subtree at point." 876 | (-any #'funcall org-multi-wiki-removal-block-functions)) 877 | 878 | ;;;###autoload 879 | (defun org-multi-wiki-create-entry-from-subtree (namespace) 880 | "Create a new entry from the current subtree. 881 | 882 | This command creates a new entry in the selected NAMESPACE, from 883 | an Org subtree outside of any wiki. 884 | 885 | After successful operation, the original subtree is deleted from 886 | the source file." 887 | (interactive (list (if (org-multi-wiki--removal-blocked-p) 888 | (user-error "You cannot move this wiki entry/subtree") 889 | (org-multi-wiki-select-namespace "Namespace: ")))) 890 | (unless (derived-mode-p 'org-mode) 891 | (user-error "Must be run inside org-mode")) 892 | (when (org-multi-wiki--removal-blocked-p) 893 | (user-error "You cannot move this wiki entry/subtree")) 894 | (let* ((heading (org-multi-wiki--cleanup-heading 895 | (org-get-heading t t t t))) 896 | ;; Let the user determine the file name. 897 | ;; 898 | ;; The user can edit the heading after the entry creation, so 899 | ;; only the file name matters at this point. 900 | (filename (read-string "Filename: " 901 | (concat (substring-no-properties 902 | (funcall org-multi-wiki-escape-file-name-fn 903 | heading)) 904 | ".org"))) 905 | ;; Append a suffix to the file name if it does not end with 906 | ;; .org or .gpg 907 | (filename (if (or (string-match-p (rx (or ".org" ".gpg") eol) filename)) 908 | filename 909 | (concat filename ".org"))) 910 | (directory (org-multi-wiki-directory namespace)) 911 | (fpath (expand-file-name filename directory))) 912 | ;; Run some verification here 913 | (unless (and directory (file-directory-p directory)) 914 | (user-error "Directory is nil or non-existent: %s" directory)) 915 | (when (file-exists-p fpath) 916 | (error "File already exists: %s" fpath)) 917 | (when (find-buffer-visiting fpath) 918 | (error "Buffer visiting the file already exists: %s" fpath)) 919 | (let ((buf (find-file-noselect fpath))) 920 | (condition-case err 921 | (progn 922 | ;; TODO: Apply the template to the new file but don't create an entry in it 923 | (org-multi-wiki--setup-new-buffer buf namespace fpath directory) 924 | (org-refile nil nil (list heading fpath nil nil)) 925 | (with-current-buffer buf 926 | (goto-char (point-min))) 927 | (funcall org-multi-wiki-display-buffer-fn buf)) 928 | (error 929 | ;; Clean up the created buffer if it has zero length 930 | (when (zerop (buffer-size buf)) 931 | (kill-buffer buf)) 932 | (error err)))))) 933 | 934 | (provide 'org-multi-wiki) 935 | ;;; org-multi-wiki.el ends here 936 | -------------------------------------------------------------------------------- /screenshots/helm-org-multi-wiki-multi-ns-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akirak/org-multi-wiki/bf8039aadddaf02569fab473f766071ef7e63563/screenshots/helm-org-multi-wiki-multi-ns-1.png --------------------------------------------------------------------------------