-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathorg-novelist.el
4894 lines (4702 loc) · 307 KB
/
org-novelist.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; org-novelist.el --- An Org mode system for writing and exporting fiction novels -*- lexical-binding: t; -*-
;; JUF's methodology for keeping novel writing nice and tidy.
;; Copyright (C) 2023 John Urquhart Ferguson
;;
;; Author: John Urquhart Ferguson <[email protected]>
;; Maintainer: John Urquhart Ferguson <[email protected]>
;; URL: https://johnurquhartferguson.info
;; Keywords: fiction, writing, outlines
;; Prefix: org-novelist
;; Package-Requires: ((emacs "28.1") (org "9.5.5"))
;; Version: 0.0.3
;; This file is not part of GNU Emacs.
;;
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see https://www.gnu.org/licenses/.
;;; Commentary:
;;
;; Org Novelist is a methodology for writing novel-length fiction using
;; Org mode within Emacs. It involves creating and laying out Org mode
;; files such that notes and plans can be easily created and quickly
;; accessed while writing the main text of a story. Org Novelist's
;; secondary function is the ability to use this known structure to
;; easily export and publish stories to other formats. This package
;; supplies a collection of support functions which make it easier to
;; use this methodology.
;;
;; Creating, linking, and laying out files in the Org Novelist
;; methodology can be done without the use of Emacs or the Org Novelist
;; package, but using the package within Emacs will provide helper
;; functions that make the methodology much easier to use; allowing the
;; following of links, programmatic updating of cross-references, and
;; the ability to programmatically export to other formats.
;;
;; Installation, Activation, and Documentation
;; -------------------------------------------
;; See the corresponding section of the website at
;;
;; https://johnurquhartferguson.info
;;; Code:
;;;; Require other packages
(require 'org) ; Org Novelist is built upon the incredible work of Org mode
(require 'ox) ; Necessary to call Org's built-in export functions.
;;;; Global Variables
(defvar orgn--autoref-p nil "Temporary store for last known value of org-novelist-automatic-referencing-p.")
(defvar orgn--lang-tag nil "Temporary store for the original language tag set for this session.")
(defvar orgn--org-version-checked-p nil "Flag to show if the Org mode version has been checked.")
(defvar orgn--org-9.6-or-above-p nil "Flag to show if Org mode version is 9.6 or above.")
(defvar orgn--emacs-version-checked-p nil "Flag to show if the Emacs version has been checked.")
(defvar orgn--emacs-29-or-above-p nil "Flag to show if Emacs version is 29 or above.")
(defvar orgn-mode-map (make-sparse-keymap) "Setup a keymap to pass to `easy-menu-define'.")
(defvar orgn-menu)
;;;; Global Constants
;; This constant allows the system to work on Linux and Windows (and any other system, though I've only tested those two). I think it's faster for Emacs than calling the `file-name-as-directory' function every time I need it, and using the alias keeps the code cleaner.
(defvaralias 'orgn--folder-separator '/ "Add in an alias to make code cleaner when constructing file locations.")
(defconst orgn--folder-separator (file-name-as-directory "/") "Assign the current system's folder separator to a global variable.")
(defconst orgn--config-filename "org-novelist-config.org" "Filename of where Org Novelist will store configuration data.")
(defconst orgn--data-filename "org-novelist-data.org" "Filename of where Org Novelist will story less mutable data.")
(defconst orgn--file-ending ".org" "The ending of the filenames used in Org Novelist.")
(defconst orgn--mode-identifier "; -*-Org-Novelist-*-" "The Emacs mode identifier for Org Novelist.")
(defconst orgn--language-tag-property "LANGUAGE_TAG" "Property key for the language tag associated with an Org Novelist story.") ; Based on https://www.w3.org/International/articles/language-tags/index.en
(defconst orgn--aliases-property "ALIASES" "Property key for the notes name aliases in a notes file.")
(defconst orgn--add-to-generators-property "ADD_TO_GENERATORS" "Property key for generators notes should be added to (eg, index, glossary).")
(defconst orgn--generate-property "GENERATE" "Property key for generators a story should apply (eg, index, glossary).")
(defconst orgn--linked-stories-property "LINKED_STORIES" "Property key to link story notes across stories.")
(defconst orgn--linked-stories-separator "|?|" "Regexp to match the separators in a list of generator story directories.")
(defconst orgn--index-entry-property "ORG_NOVELIST_INDEX_ENTRY" "Property key for an index entry.")
(defconst orgn--matter-type-property "ORG-NOVELIST-MATTER-TYPE" "Property key for the chapter matter type in the export property drawer.")
(defconst orgn--front-matter-value "FRONT MATTER" "Name for the Front Matter value in the export property drawer.")
(defconst orgn--main-matter-value "MAIN MATTER" "Name for the Main Matter value in the export property drawer.")
(defconst orgn--back-matter-value "BACK MATTER" "Name for the Back Matter value in the export property drawer.")
(defconst orgn--index-generator-value "index" "Value for applying an index generator.")
(defconst orgn--glossary-generator-value "glossary" "Value for applying a glossary generator.")
(defconst orgn--language-packs-folder "language-packs" "The folder relative to org-novelist.el which stores the language packs.")
(defconst orgn--language-pack-file-prefix "org-novelist-language-pack-" "The prefix or an Org Novelist language pack file.")
;;;; Language Packs
;;;; British English (en-GB)
;; User Variable Fallback Strings
(defconst orgn--author-not-set-en-GB "Author Not Set" "Author not specified by user.")
(defconst orgn--author-email-not-set-en-GB "Author Email Not Set" "Author email not specified by user.")
;; File Instructions
(defconst orgn--main-file-instructions-en-GB "Write a brief summary of the story here" "Instructions for the main entry-point file.")
(defconst orgn--notes-file-instructions-en-GB "Write any general notes for the story here" "Instructions for the general notes file.")
(defconst orgn--research-file-instructions-en-GB "Write any research notes for the story here" "Instructions for the general research file.")
(defconst orgn--characters-file-instructions-en-GB "This is an index of the characters in the story. Do not edit manually. Use only Org mode or Org Novelist functions." "Instructions for the character index file.")
(defconst orgn--places-file-instructions-en-GB "This is an index of the places in the story. Do not edit manually. Use only Org mode or Org Novelist functions." "Instructions for the location index file.")
(defconst orgn--props-file-instructions-en-GB "This is an index of the props in the story. Do not edit manually. Use only Org mode or Org Novelist functions." "Instructions for the prop index file.")
(defconst orgn--chapters-file-instructions-en-GB "This is an index of the chapters in the story. Do not edit manually. Use only Org mode or Org Novelist functions." "Instructions for the chapter index file.")
(defconst orgn--linked-stories-file-instructions-en-GB "This is an index of linked stories. Do not edit manually. Use only Org mode or Org Novelist functions." "Instructions for the linked stories index file.")
;; Folder Names
(defconst orgn--notes-folder-en-GB "Notes" "The folder name for storing note files.")
(defconst orgn--indices-folder-en-GB "Indices" "The folder name for storing index files.")
(defconst orgn--chapters-folder-en-GB "Chapters" "The folder name for storing the chapter files.")
(defconst orgn--exports-folder-en-GB "Exports" "The folder name for storing export files.")
;; File Names
(defconst orgn--main-file-en-GB "main" "Name for the story's main entry-point file.")
(defconst orgn--notes-file-en-GB "notes" "Name for the story's general notes file.")
(defconst orgn--research-file-en-GB "research" "Name for the story's general research file.")
(defconst orgn--characters-file-en-GB "characters" "Name for the story's character index file.")
(defconst orgn--places-file-en-GB "places" "Name for the story's location index file.")
(defconst orgn--props-file-en-GB "props" "Name for the story's prop index file.")
(defconst orgn--chapters-file-en-GB "chapters" "Name for the story's chapter index file.")
(defconst orgn--linked-stories-file-en-GB "linked-stories" "Name for the story's linked stories index file.")
(defconst orgn--chapter-file-prefix-en-GB "chapter-" "Prefix for the story's chapter files.")
(defconst orgn--notes-suffix-en-GB "-notes" "Suffix for a file's associated notes file.")
(defconst orgn--character-file-prefix-en-GB "character-" "Prefix for the story's character files.")
(defconst orgn--prop-file-prefix-en-GB "prop-" "Prefix for the story's prop files.")
(defconst orgn--place-file-prefix-en-GB "place-" "Prefix for the story's place files.")
;; File Titles
(defconst orgn--notes-title-en-GB "Notes" "Name for the story's general notes title.")
(defconst orgn--research-title-en-GB "Research" "Name for the story's general research title.")
(defconst orgn--characters-title-en-GB "Characters" "Name for the story's character index title.")
(defconst orgn--places-title-en-GB "Places" "Name for the story's location index title.")
(defconst orgn--props-title-en-GB "Props" "Name for the story's prop index title.")
(defconst orgn--chapters-title-en-GB "Chapters" "Name for the story's chapter index title.")
(defconst orgn--config-name-en-GB "Export Settings" "Display name for a link to the story's configuration file.")
(defconst orgn--linked-stories-title-en-GB "Linked Stories" "Name for the story's linked stories index title.")
;; File Preambles
(defconst orgn--story-name-en-GB "story name" "Placeholder for the name of the story, used in generating template preambles.")
(defconst orgn--chapter-name-en-GB "chapter name" "Placeholder for the name of a chapter, used in generating template preambles.")
;; <<story name>> (without the << >> brackets) must share the same value as org-novelist--story-name-en-GB.
;; <<chapter name>> (without the << >> brackets) must share the same value as org-novelist--chapter-name-en-GB.
(defconst orgn--notes-for-story-name-en-GB "Notes for <<story name>>" "Part of the preamble for the general notes file.")
(defconst orgn--notes-for-chapter-name-en-GB "Notes for <<chapter name>>" "Part of the preamble for the chapter notes file.")
(defconst orgn--research-for-story-name-en-GB "Research for <<story name>>" "Part of the preamble for the general research file.")
(defconst orgn--character-index-for-story-name-en-GB "Character Index for <<story name>>" "Part of the preamble for the character index file.")
(defconst orgn--place-index-for-story-name-en-GB "Place Index for <<story name>>" "Part of the preamble for the location index file.")
(defconst orgn--prop-index-for-story-name-en-GB "Prop Index for <<story name>>" "Part of the preamble for the prop index file.")
(defconst orgn--chapter-index-for-story-name-en-GB "Chapter Index for <<story name>>" "Part of the preamble for the chapter index file.")
(defconst orgn--linked-stories-index-for-story-name-en-GB "Linked Stories Index for <<story name>>" "Part of the preamble for the linked stories index file.")
(defconst orgn--front-matter-heading-en-GB "Front Matter" "Name for the Front Matter of the book chapters, used as a heading.")
(defconst orgn--main-matter-heading-en-GB "Main Matter" "Name for the Main Matter of the book chapters, used as a heading.")
(defconst orgn--back-matter-heading-en-GB "Back Matter" "Name for the Back Matter of the book chapters, used as a heading.")
(defconst orgn--notes-en-GB "Notes" "Part of the preamble for a chapter file.")
(defconst orgn--chapter-en-GB "chapter" "Part of the preamble for a chapter file.")
(defconst orgn--character-en-GB "character" "Part of the preamble for a character file.")
(defconst orgn--prop-en-GB "prop" "Part of the preamble for a prop file.")
(defconst orgn--place-en-GB "place" "Part of the preamble for a place file.")
(defconst orgn--character-name-en-GB "character name" "Placeholder for the name of a character, used in generating template preambles.")
(defconst orgn--prop-name-en-GB "prop name" "Placeholder for the name of a prop, used in generating template preambles.")
(defconst orgn--place-name-en-GB "place name" "Placeholder for the name of a place, used in generating template preambles.")
;; <<Notes>> (without the << >> brackets) must share the same value as org-novelist--notes-en-GB.
;; <<chapter>> (without the << >> brackets) must share the same value as org-novelist--chapter-en-GB.
;; <<story name>> (without the << >> brackets) must share the same value as org-novelist--story-name-en-GB.
;; <<chapter name>> (without the << >> brackets) must share the same value as org-novelist--chapter-name-en-GB.
;; <<character name>> (without the << >> brackets) must share the same value as org-novelist--character-name-en-GB.
;; <<character>> (without the << >> brackets) must share the same value as org-novelist--character-en-GB.
;; <<prop name>> (without the << >> brackets) must share the same value as org-novelist--prop-name-en-GB.
;; <<prop>> (without the << >> brackets) must share the same value as org-novelist--prop-en-GB.
;; <<place name>> (without the << >> brackets) must share the same value as org-novelist--place-name-en-GB.
;; <<place>> (without the << >> brackets) must share the same value as org-novelist--place-en-GB.
(defconst orgn--notes-are-available-for-this-chapter-from-story-name-en-GB "<<Notes>> are available for this <<chapter>> from <<story name>>." "Sentence describing chapter notes availability.")
(defconst orgn--notes-for-chapter-name-a-chapter-from-story-name-en-GB "Notes for <<chapter name>>, a <<chapter>> from <<story name>>." "Sentence header for chapter notes template.")
(defconst orgn--notes-for-character-name-a-character-from-story-name-en-GB "Notes for <<character name>>, a <<character>> from <<story name>>." "Sentence header for character notes template.")
(defconst orgn--notes-for-prop-name-a-prop-from-story-name-en-GB "Notes for <<prop name>>, a <<prop>> from <<story name>>." "Sentence header for prop notes template.")
(defconst orgn--notes-for-place-name-a-place-from-story-name-en-GB "Notes for <<place name>>, a <<place>> from <<story name>>." "Sentence header for place notes template.")
(defconst orgn--content-header-en-GB "Content" "Part of the preamble for a chapter file.")
(defconst orgn--scene-name-here-en-GB "Scene Name Here" "Part of the preamble for a chapter file.")
(defconst orgn--glossary-header-en-GB "Glossary" "Part of the writing glossary in chapter files.")
(defconst orgn--view-notes-en-GB "View Notes" "A link text to let the user view related notes.")
(defconst orgn--new-name-en-GB "New name" "Placeholder for the new name in an alias string.")
(defconst orgn--old-name-en-GB "old name" "Placeholder for the old name in an alias string.")
;; <<New name>> (without the << >> brackets) must share the same value as org-novelist--new-name-en-GB.
;; <<old name>> (without the << >> brackets) must share the same value as org-novelist--old-name-en-GB.
(defconst orgn--new-name-is-an-alias-for-old-name-en-GB "<<New name>> is an alias for <<old name>>." "Text to let the user know something is an alias.")
(defconst orgn--appearances-in-chapters-header-en-GB "Appearances in Chapters" "Part of the references section in notes files.")
(defconst orgn--line-en-GB "Line" "The word for the line of a chapter. Used at the start of a sentence.")
(defconst orgn--not-yet-referenced-en-GB "Not yet referenced in story." "Display that an object has not yet been mentioned in any of the chapter files.")
(defconst orgn--exports-header-en-GB "Exports" "Heading for configuration file to use to list export templates.")
;; File Content
(defconst orgn--chapter-notes-content-en-GB
(concat
"Show how this chapter contributes to:\n"
"** Character Development\n"
"** Moving the Plot Forward\n"
"** Enriching the Setting\n")
"Starter content for the chapter notes files.")
(defconst orgn--character-notes-content-en-GB
(concat
"** Role in Story\n"
"** What Does This Character Want?\n"
"** What Would Most Motivate This Character Into Taking Action?\n"
"** What Would Most Prevent This Character From Taking Action?\n"
"** What Is The Worst Thing That Could Happen To This Character?\n"
"** What Is The Best Thing That Could Happen To This Character?\n"
"** Who or What Is Stopping This Character From Getting What They Want?\n"
"** What Does This Character Need To Learn In Order To Be Happy?\n"
"** Occupation\n"
"** Physical Description\n"
"** Personality\n"
"** Habits/Mannerisms\n"
"** Background\n"
"** Internal Conflicts\n"
"** External Conflicts\n"
"** Notes\n")
"Starter content for the character notes files.")
(defconst orgn--prop-notes-content-en-GB
(concat
"** Role in Story\n"
"** Description\n"
"** Background\n"
"** Notes\n")
"Starter content for the prop notes files.")
(defconst orgn--place-notes-content-en-GB
(concat
"** Role in Story\n"
"** Description\n"
"** Background\n"
"** Related Characters\n"
"** Season\n"
"** Unique Features\n"
"** Sights\n"
"** Sounds\n"
"** Smells\n"
"** Notes\n")
"Starter content for the place notes files.")
(defconst orgn--alias-en-GB "Alias" "Alias section announcement for glossaries.")
(defconst orgn--glossary-default-character-desc-en-GB "A character in the story." "The default description in the index for a character in the story.")
(defconst orgn--glossary-default-place-desc-en-GB "A place in the story." "The default description in the index for a place in the story.")
(defconst orgn--glossary-default-prop-desc-en-GB "A prop in the story." "The default description in the index for a prop in the story.")
;; User Queries
(defconst orgn--story-name-query-en-GB "Story Name?" "A query to the user for what to name their story.")
(defconst orgn--story-save-location-query-en-GB "Story Save Location?" "A query to the user for where to save their story.")
(defconst orgn--chapter-name-query-en-GB "Chapter Name?" "A query to the user for the name of a chapter.")
(defconst orgn--chapter-location-query-en-GB "Choose Chapter Location From Available Options for \"%s\" (%s/%s/%s):" "A query to the user for what section in which to place a new chapter.")
(defconst orgn--rebuild-chapter-index-location-query-en-GB "Rebuilding index: Where should chapters go?" "When rebuilding chapter index, ask user where to place chapters.")
(defconst orgn--file-by-file-en-GB "Select individually for each file" "Offer to the user to make selections on a file by file basis.")
(defconst orgn--delete-file-query-en-GB "Delete file?" "A query to show the user to see if they want to delete a file.")
(defconst orgn--name-already-in-use-en-GB "That name is already in use. Please try again" "Tell user the chosen name is already in use.")
(defconst orgn--okay-en-GB "Okay" "Positive acknowledgement to the user.") ; This is also used to check that a language pack exists
(defconst orgn--new-chapter-name-query-en-GB "New Chapter Name?" "A query to the user for the new name of a chapter.")
(defconst orgn--character-name-query-en-GB "Character Name?" "A query to the user for what to name a character.")
(defconst orgn--prop-name-query-en-GB "Prop Name?" "A query to the user for what to name a prop.")
(defconst orgn--place-name-query-en-GB "Place Name?" "A query to the user for what to name a place.")
(defconst orgn--new-character-name-query-en-GB "New Character Name?" "A query to the user for the new name of a character.")
(defconst orgn--new-prop-name-query-en-GB "New Prop Name?" "A query to the user for the new name of a prop.")
(defconst orgn--new-place-name-query-en-GB "New Place Name?" "A query to the user for the new name of a place.")
(defconst orgn--new-story-name-query-en-GB "New Story Name?" "A query to the user for the new name for the story.")
(defconst orgn--rename-story-folder-query-en-GB "Rename story folder as well?" "A query to the user whether to also rename the story folder.")
(defconst orgn--match-lang-tag-to-story-query-en-GB "What language was used to create this story (eg, 'en-GB')?" "A query to the user to change the session language tag.")
(defconst orgn--story-folder-to-link-to-query-en-GB "Story folder to link to current story?" "A query to the user for the story folder where a story to be linked is located.")
(defconst orgn--unlink-from-which-story-query-en-GB "Unlink from which story?" "A query to the user for which story to unlink from the current story.")
;; Error/Throw/Messages
(defconst orgn--function-name-en-GB "function name" "Placeholder for the name of the function, used in generating error messages.")
(defconst orgn--filename-en-GB "filename" "Placeholder for the filename, used in generating error messages.")
;; <<function name>> (without the << >> brackets) must share the same value as org-novelist--function-name-en-GB.
(defconst orgn--no-localised-function-en-GB "No localised function found for <<function name>>" "The local language version of the function is missing.")
;; <<filename>> (without the << >> brackets) must share the same value as org-novelist--filename-en-GB.
(defconst orgn--filename-is-not-writable-en-GB "<<filename>> is not writable" "File is not writable.")
(defconst orgn--story-folder-already-in-use-en-GB "That story folder is already in use" "Tell user the selected folder already contains an Org Novelist story.")
;; <<filename>> (without the << >> brackets) must share the same value as org-novelist--filename-en-GB.
(defconst orgn--filename-is-not-part-of-a-story-folder-en-GB "<<filename>> is not part of an Org Novelist story folder" "Function run from location not appearing to be part of an Org Novelist story.")
(defconst orgn--no-story-found-en-GB "No story found" "No story found in folder.")
;; <<filename>> (without the << >> brackets) must share the same value as org-novelist--filename-en-GB.
(defconst orgn--filename-is-not-readable-en-GB "<<filename>> is not readable" "File is not readable.")
(defconst orgn--new-chapter-created-en-GB "New chapter created" "Throw out of chapter creation loop once chapter created. Not an error.")
(defconst orgn--no-more-headings-en-GB "No more headings" "Throw out of chapter creation loop as section heading not found. Not an error.")
(defconst orgn--file-malformed-en-GB "File malformed" "Throw out of chapter creation function as no top heading. Recoverable error.")
(defconst orgn--file-not-found-en-GB "File not found" "The requested file could not be found.")
(defconst orgn--no-chapters-found-en-GB "No chapters found" "No chapters found in story.")
(defconst orgn--unsaved-buffer-en-GB "Unsaved buffer" "Description of a buffer that is not saved to disk.")
(defconst orgn--no-characters-found-en-GB "No characters found" "No characters found in story.")
(defconst orgn--no-props-found-en-GB "No props found" "No props found in story.")
(defconst orgn--no-places-found-en-GB "No places found" "No places found in story.")
;; <<filename>> (without the << >> brackets) must share the same value as org-novelist--filename-en-GB.
(defconst orgn--filename-is-not-a-recognised-index-en-GB "<<filename>> is not a recognised index" "Index is not of a known type.")
(defconst orgn--auto-ref-now-on-en-GB "Org Novelist automatic referencing has been turned ON" "Inform user that automatic referencing has been turned on.")
(defconst orgn--auto-ref-now-off-en-GB "Org Novelist automatic referencing has been turned OFF" "Inform user that automatic referencing has been turned off.")
(defconst orgn--language-tag-en-GB "language tag" "Placeholder for the language code, used in generating error messages.") ; Based on https://www.w3.org/International/articles/language-tags/index.en
;; <<language tag>> (without the << >> brackets) must share the same value as org-novelist--language-tag-en-GB.
(defconst orgn--language-set-to-language-tag-en-GB "Org Novelist language set to: <<language tag>>" "Inform user that language has been set.")
(defconst orgn--language-not-found-en-GB "Selected language pack not found." "Inform user that language pack could not be found.")
(defconst orgn--chosen-story-same-as-current-story-en-GB "Chosen story is the same as the current story." "Inform the user that they've selected the current story, instead of a new one.")
(defconst orgn--folder-already-exists-en-GB "That folder already exists" "Inform user the folder already exists.")
(defconst orgn--no-linked-stories-en-GB "Currently not linked to any stories" "Inform user there are currently no linked stories.")
;; Pattern Matches
(defconst orgn--sys-safe-name-en-GB "[-A-Za-z0-9]*" "Regexp to match strings produced by `org-novelist--system-safe-name-en-GB'.")
(defconst orgn--aliases-separators-en-GB "[,\f\t\n\r\v]+" "Regexp to match the separators in a list of aliases.")
(defconst orgn--generate-separators-en-GB orgn--aliases-separators-en-GB "Regexp to match the separators in a list of generators.")
(defconst orgn--notes-name-search-en-GB "[[:space:][:punct:]]+?%s[[:space:][:punct:]]+?" "Regexp to match names of things in chapter files.")
(defconst orgn--notes-name-org-link-search-en-GB "\\[\\[:space:\\]\\[:punct:\\]\\]+?%s\\[\\[:space:\\]\\[:punct:\\]\\]+?" "Regexp to match, from an Org mode link, names of things in chapter files.")
;;;; Internationalised Functions
(defun orgn--system-safe-name-en-GB (str)
"Convert STR to a directory safe name.
The resultant string should be suitable for all computer systems using en-GB."
;; I'm just converting things to CamelCase at the moment, and removing non-Latin alphabet characters.
;; I've tried to replace special characters with simpler transliterated equivalents that the camelise function can work with ([-A-Za-z0-9]*).
;; Since British English regularly loans words from French, German, Spanish, etc, I've tried to do my best to resolve a sensible list of equivalencies.
;; Make sure that the language pack constant `org-novelist--sys-safe-name-en-GB' matches the output of this function.
(let ((special-chars (make-hash-table :test 'equal))
(case-fold-search nil))
(puthash "£" "GBP" special-chars)
(puthash "€" "EUR" special-chars)
(puthash "$" "USD" special-chars)
(puthash "Ð" "D" special-chars) ; DH might be better
(puthash "ð" "d" special-chars) ; dh might be better
(puthash "₫" "dd" special-chars)
(puthash "Þ" "Th" special-chars)
(puthash "þ" "th" special-chars)
(puthash "Õ" "O" special-chars)
(puthash "õ" "o" special-chars)
(puthash "Ã" "A" special-chars)
(puthash "ã" "a" special-chars)
(puthash "Ø" "Oe" special-chars)
(puthash "ø" "oe" special-chars)
(puthash "Ù" "U" special-chars)
(puthash "ù" "u" special-chars)
(puthash "Ò" "O" special-chars)
(puthash "ò" "o" special-chars)
(puthash "Ì" "I" special-chars)
(puthash "ì" "i" special-chars)
(puthash "Å" "AA" special-chars)
(puthash "å" "aa" special-chars)
(puthash "Á" "A" special-chars)
(puthash "á" "a" special-chars)
(puthash "Í" "I" special-chars)
(puthash "í" "i" special-chars)
(puthash "Ó" "O" special-chars)
(puthash "ó" "o" special-chars)
(puthash "Ú" "U" special-chars)
(puthash "ú" "u" special-chars)
(puthash "ý" "y" special-chars)
(puthash "Ñ" "N" special-chars)
(puthash "ñ" "n" special-chars)
(puthash "Ÿ" "Y" special-chars)
(puthash "ÿ" "y" special-chars)
(puthash "Ù" "U" special-chars)
(puthash "ù" "u" special-chars)
(puthash "Û" "U" special-chars)
(puthash "û" "u" special-chars)
;; (puthash "Ü" "u" special-chars) ; Not used here. I'm opting to give German priority as English is more closely related to Germanic than romance languages
;; (puthash "ü" "u" special-chars) ; Not used here. I'm opting to give German priority as English is more closely related to Germanic than romance languages
(puthash "Ô" "O" special-chars)
(puthash "ô" "o" special-chars)
(puthash "Œ" "OE" special-chars)
(puthash "œ" "oe" special-chars)
(puthash "Î" "I" special-chars)
(puthash "î" "i" special-chars) ; For Italian, it might be better to replace this with a double i, though in other languages a single i seems fair
(puthash "Ï" "I" special-chars)
(puthash "ï" "i" special-chars)
(puthash "É" "E" special-chars)
(puthash "é" "e" special-chars)
(puthash "È" "E" special-chars)
(puthash "è" "e" special-chars)
(puthash "Ê" "E" special-chars)
(puthash "ê" "e" special-chars)
(puthash "Ë" "E" special-chars)
(puthash "ë" "e" special-chars)
(puthash "Ç" "C" special-chars)
(puthash "ç" "c" special-chars)
(puthash "À" "A" special-chars)
(puthash "à" "a" special-chars)
(puthash "Â" "A" special-chars)
(puthash "â" "a" special-chars)
(puthash "Æ" "AE" special-chars)
(puthash "æ" "ae" special-chars)
(puthash "Ä" "Ae" special-chars)
(puthash "Ö" "Oe" special-chars)
(puthash "Ü" "Ue" special-chars)
(puthash "ä" "ae" special-chars)
(puthash "ö" "oe" special-chars)
(puthash "ü" "ue" special-chars)
(puthash "ẞ" "SS" special-chars)
(puthash "ß" "ss" special-chars)
(puthash "ſ" "s" special-chars)
(puthash "ʒ" "z" special-chars)
(setq str (orgn--replace-chars special-chars str))
(orgn--camelise str)))
(defun orgn--camelise-en-GB (str)
"Convert STR to CamelCase, using only the Latin alphabet.
The resultant string should be suitable for all computer systems using en-GB."
(let* ((white-list (list "A" "a" "B" "b" "C" "c" "D" "d" "E" "e" "F" "f" "G" "g" "H" "h" "I" "i" "J" "j" "K" "k" "L" "l" "M" "m" "N" "n" "O" "o" "P" "p" "Q" "q" "R" "r" "S" "s" "T" "t" "U" "u" "V" "v" "W" "w" "X" "x" "Y" "y" "Z" "z" "0" "1" "2" "3" "4" "5" "6" "7" "8" "9" " ")) ; List of allowed characters in folder names, plus the space character. The space will also be removed later on, but we need it in the white list as it will be used as a word separator.
(taboo-chars (mapcar 'string (string-to-list (orgn--remove-chars white-list str))))) ; Add characters to this list that are not in the white list
(mapconcat 'identity (mapcar
(lambda (word) (capitalize (downcase word)))
(split-string (orgn--remove-chars taboo-chars str) " ")) nil)))
;;;; British English (en-GB) ends here
;;;; Localisation Functions
(defun orgn--localise-string (str-name &rest str-list)
"Return the correct language version of a string.
More strings can be included with STR-LIST, and the results will be concatenated
into one string. To change the language, the variable
`org-novelist-language-tag' must be set to a supported language for STR-NAME.
The default is \"en-GB\".
Strings matching the values of `org-novelist--folder-separator' or
`org-novelist--file-ending' will be returned without change."
(catch 'LOCALISATION-STRING-NOT-FOUND
(let ((not-in-story-folder-p nil))
;; Check for language tag stored in data file.
(catch 'NO-STORY-ROOT-FOUND
(let (current-folder
story-language-tag)
(unless (string= " *temp*" (buffer-file-name))
(when (or load-file-name buffer-file-name)
(setq current-folder (directory-file-name (file-name-directory (or load-file-name buffer-file-name))))))
(when current-folder
(while (not (file-exists-p (concat current-folder / orgn--config-filename)))
(when (string= current-folder (setq current-folder (expand-file-name (concat current-folder / ".." ))))
(setq not-in-story-folder-p t)
(throw 'NO-STORY-ROOT-FOUND current-folder)))
;; If we get to this point, a story root folder was found and that is what current-folder is set to.
(setq story-language-tag (orgn--get-file-property-value orgn--language-tag-property (concat (expand-file-name current-folder) / orgn--data-filename)))
(if (not (string= "" story-language-tag))
;; A value was found for the language tag of this story. So set it up.
(progn
;; Store original user set language in case we move back to another story with no set language tag.
(unless orgn--lang-tag
(if (boundp 'orgn-language-tag)
(setq orgn--lang-tag orgn-language-tag)
(setq orgn--lang-tag "en-GB")))
(setq orgn-language-tag story-language-tag))
;; This story has no language tag, so revert back to user set language tag.
(when orgn--lang-tag
(setq orgn-language-tag orgn--lang-tag))))))
;; If we're not in a story folder, and lang-tag is bound, use lang-tag.
(when (and not-in-story-folder-p (boundp 'orgn--lang-tag))
(setq orgn-language-tag orgn--lang-tag))
;; Fallback in case the above code didn't figure out the language.
(unless (boundp 'orgn-language-tag)
(defvar orgn-language-tag "en-GB" "The language to use for Org Novelist. Based on https://www.w3.org/International/articles/language-tags/index.en"))
(unless orgn-language-tag
(setq orgn-language-tag "en-GB"))
;; End of fallback.
(unless (boundp (intern (concat "org-novelist--okay-" orgn-language-tag)))
;; Language tag is set, but the language pack isn't loaded. Try to load it from standard location.
(when (file-readable-p (expand-file-name (concat (file-name-directory (symbol-file 'org-novelist--localise-string)) orgn--language-packs-folder / orgn--language-pack-file-prefix (downcase orgn-language-tag) ".el")))
(load-file (expand-file-name (concat (file-name-directory (symbol-file 'org-novelist--localise-string)) orgn--language-packs-folder / orgn--language-pack-file-prefix (downcase orgn-language-tag) ".el"))))
(unless (boundp (intern (concat "org-novelist--okay-" orgn-language-tag)))
(setq orgn-language-tag "en-GB")
(message (concat (orgn--ls "language-not-found") " " (orgn--replace-string-in-string (concat "<<" (orgn--ls "language-tag") ">>") orgn-language-tag (orgn--ls "language-set-to-language-tag"))))))
(cond ((string-equal str-name /) ; Special case for the folder separator string
(if (> (length str-list) 0)
(concat (eval /) (apply 'orgn--localise-string str-list))
(eval /)))
((string-equal str-name orgn--file-ending) ; Special case for the Org Novelist filename ending
(if (> (length str-list) 0)
(concat (eval orgn--file-ending) (apply 'orgn--localise-string str-list))
(eval orgn--file-ending)))
((boundp (intern (concat "org-novelist--" str-name "-" orgn-language-tag))) ; Do not shorten this string to orgn-- as it will prevent running outwith Org Novelist mode
(if (> (length str-list) 0)
(concat (eval (intern (concat "org-novelist--" str-name "-" orgn-language-tag))) (apply 'orgn--localise-string str-list)) ; Do not shorten this string to orgn-- as it will prevent running outwith Org Novelist mode.
(eval (intern (concat "org-novelist--" str-name "-" orgn-language-tag))))) ; Do not shorten this string to orgn-- as it will prevent running outwith Org Novelist mode
(t
;; The two lines of code below are the only user-facing ones that can't be translated.
(error (format "No localised string for '%s' found" str-name))
(throw 'LOCALISATION-STRING-NOT-FOUND (format "No localised string for '%s' found" str-name)))))))
(defalias 'orgn--ls 'orgn--localise-string) ; Make an alias to keep code a little cleaner
(defun orgn--force-localise-string (str-name forced-lang-code &rest str-list)
"Return the correct language version of STR-NAME for FORCED-LANG-CODE.
More strings can be included with STR-LIST, and the results will be concatenated
into one string.
Strings matching the values of `org-novelist--folder-separator' or
`org-novelist--file-ending' will be returned without change."
(catch 'LOCALISATION-STRING-NOT-FOUND
(unless (boundp (intern (concat "org-novelist--okay-" forced-lang-code)))
;; Language pack isn't loaded. Try to load it from standard location.
(when (file-readable-p (expand-file-name (concat (file-name-directory (symbol-file 'org-novelist--force-localise-string)) orgn--language-packs-folder / orgn--language-pack-file-prefix (downcase forced-lang-code) ".el")))
(load-file (expand-file-name (concat (file-name-directory (symbol-file 'org-novelist--force-localise-string)) orgn--language-packs-folder / orgn--language-pack-file-prefix (downcase forced-lang-code) ".el"))))
(unless (boundp (intern (concat "org-novelist--okay-" forced-lang-code)))
;; The two lines of code below are the only user-facing ones that can't be translated.
(error (format "No localised string for '%s' found" str-name))
(throw 'LOCALISATION-STRING-NOT-FOUND (format "No localised string for '%s' found" str-name))))
(cond ((string-equal str-name /) ; Special case for the folder separator string
(if (> (length str-list) 0)
(concat (eval /) (apply 'orgn--force-localise-string (car str-list) forced-lang-code (cdr str-list)))
(eval /)))
((string-equal str-name orgn--file-ending) ; Special case for the Org Novelist filename ending
(if (> (length str-list) 0)
(concat (eval orgn--file-ending) (apply 'orgn--force-localise-string (car str-list) forced-lang-code (cdr str-list)))
(eval orgn--file-ending)))
((boundp (intern (concat "org-novelist--" str-name "-" forced-lang-code))) ; Do not shorten this string to orgn-- as it will prevent running outwith Org Novelist mode
(if (> (length str-list) 0)
(concat (eval (intern (concat "org-novelist--" str-name "-" forced-lang-code))) (apply 'orgn--force-localise-string (car str-list) forced-lang-code (cdr str-list))) ; Do not shorten this string to orgn-- as it will prevent running outwith Org Novelist mode.
(eval (intern (concat "org-novelist--" str-name "-" forced-lang-code))))) ; Do not shorten this string to orgn-- as it will prevent running outwith Org Novelist mode
(t
;; The two lines of code below are the only user-facing ones that can't be translated.
(error (format "No localised string for '%s' found" str-name))
(throw 'LOCALISATION-STRING-NOT-FOUND (format "No localised string for '%s' found" str-name))))))
(defalias 'orgn--fls 'orgn--force-localise-string) ; Make an alias to keep code a little cleaner
(defun orgn--localise-function (func-name)
"Return the local language version of a function FUNC-NAME.
To change the language, the variable `org-novelist-language-tag' must be set to
a supported language. The default is \"en-GB\"."
(catch 'LOCALISATION-FUNCTION-NOT-FOUND
;; Check for language tag stored in data file.
(catch 'NO-STORY-ROOT-FOUND
(let (current-folder
story-language-tag)
(unless (string= " *temp*" (buffer-file-name))
(when (or load-file-name buffer-file-name)
(setq current-folder (directory-file-name (file-name-directory (or load-file-name buffer-file-name))))))
(when current-folder
(while (not (file-exists-p (concat current-folder / orgn--config-filename)))
(when (string= current-folder (setq current-folder (expand-file-name (concat current-folder / ".." ))))
(throw 'NO-STORY-ROOT-FOUND current-folder)))
;; If we get to this point, a story root folder was found and that is what current-folder is set to.
(setq story-language-tag (orgn--get-file-property-value orgn--language-tag-property (concat (expand-file-name current-folder) / orgn--data-filename)))
(if (not (string= "" story-language-tag))
;; A value was found for the language tag of this story. So set it up.
(progn
;; Store original user set language in case we move back to another story with no set language tag.
(unless orgn--lang-tag
(if (boundp 'orgn-language-tag)
(setq orgn--lang-tag orgn-language-tag)
(setq orgn--lang-tag "en-GB")))
(setq orgn-language-tag story-language-tag))
;; This story has no language tag, so revert back to user set language tag.
(when orgn--lang-tag
(setq orgn-language-tag orgn--lang-tag))))))
(unless (boundp 'orgn-language-tag)
(defconst orgn-language-tag "en-GB" "The language to use for Org Novelist (based on https://www.w3.org/International/articles/language-tags/index.en)."))
(unless orgn-language-tag
(setq orgn-language-tag "en-GB"))
(unless (boundp (intern (concat "org-novelist--okay-" orgn-language-tag)))
;; Language tag is set, but the language pack isn't loaded. Try to load it from standard location.
(when (file-readable-p (expand-file-name (concat (file-name-directory (symbol-file 'org-novelist--localise-function)) orgn--language-packs-folder / orgn--language-pack-file-prefix (downcase orgn-language-tag) ".el")))
(load-file (expand-file-name (concat (file-name-directory (symbol-file 'org-novelist--localise-function)) orgn--language-packs-folder / orgn--language-pack-file-prefix (downcase orgn-language-tag) ".el"))))
(unless (boundp (intern (concat "org-novelist--okay-" orgn-language-tag)))
(setq orgn-language-tag "en-GB")
(message (concat (orgn--ls "language-not-found") " " (orgn--replace-string-in-string (concat "<<" (orgn--ls "language-tag") ">>") orgn-language-tag (orgn--ls "language-set-to-language-tag"))))))
(if (fboundp (intern (concat func-name "-" orgn-language-tag)))
(intern (concat func-name "-" orgn-language-tag))
(progn
(error (orgn--replace-string-in-string (concat "<<" (orgn--ls "function-name") ">>") func-name (orgn--ls "no-localised-function")))
(throw 'LOCALISATION-FUNCTION-NOT-FOUND (orgn--replace-string-in-string (concat "<<" (orgn--ls "function-name") ">>") func-name (orgn--ls "no-localised-function")))))))
(defalias 'orgn--lf 'orgn--localise-function) ; Make an alias to keep code a little cleaner
;;;; Customisation variables
(defgroup org-novelist nil
"Helper functions for novel writing with Org mode."
:tag "Org-Novelist"
:prefix "org-novelist-"
:group 'Text
:group 'Applications)
(defcustom orgn-language-tag "en-GB"
"The language to use for Org Novelist.
Based on https://www.w3.org/International/articles/language-tags/index.en
A corresponding language pack must be included with Org Novelist."
:group 'org-novelist
:type 'string)
(defcustom orgn-author (orgn--ls "author-not-set")
"The author name you wish to appear on your stories."
:group 'org-novelist
:type 'string)
(defcustom orgn-author-email (orgn--ls "author-email-not-set")
"The contact email you wish to appear on your stories."
:group 'org-novelist
:type 'string)
(defcustom orgn-automatic-referencing-p nil
"Set to t for Org Novelist to automatically update all cross-references."
:group 'org-novelist
:type 'boolean)
(defcustom orgn-user-chapter-notes-content nil
"Override the default chapter notes template with your own contents."
:group 'org-novelist
:type 'string)
(defcustom orgn-user-character-notes-content nil
"Override the default character notes template with your own contents."
:group 'org-novelist
:type 'string)
(defcustom orgn-user-place-notes-content nil
"Override the default place notes template with your own contents."
:group 'org-novelist
:type 'string)
(defcustom orgn-user-prop-notes-content nil
"Override the default prop notes template with your own contents."
:group 'org-novelist
:type 'string)
;;;; String Manipulation Worker Functions
(defun orgn--replace-string-in-string (old-str new-str content &optional fixedcase)
"Given a string, CONTENT, replace any occurrences of OLD-STR with NEW-STR.
If FIXEDCASE is non-nil, do not alter the case of the replacement text."
;; This function was written as a non-regexp version of (replace-regexp-in-string REGEXP REP STRING).
(unless old-str
(setq old-str ""))
(unless new-str
(setq new-str ""))
(unless content
(setq content ""))
(with-temp-buffer
(insert content)
(goto-char (point-min))
(while (search-forward old-str nil t)
(replace-match new-str fixedcase t))
(buffer-string)))
(defun orgn--remove-chars (char-list str)
"Remove all instances of characters in CHAR-LIST from STR."
(let (ch)
(while char-list
(setq ch (car char-list))
(setq char-list (cdr char-list))
(setq str (orgn--replace-string-in-string ch "" str)))
str))
(defun orgn--replace-chars (char-hash str)
"Replace all keys from CHAR-HASH in STR with their associated values."
(orgn--generate-string-from-template char-hash str t))
(defun orgn--camelise (str)
"Convert STR to CamelCase, only using characters that are allowed in filenames."
;; Do not shorten this string to orgn--camelise as it will prevent running outwith Org Novelist mode.
(funcall (orgn--lf "org-novelist--camelise") str))
(defun orgn--system-safe-name (str)
"Convert STR to a directory safe name.
The resultant string should be suitable for the current operating system."
(funcall (orgn--lf "org-novelist--system-safe-name") str))
(defun orgn--sanitize-string (str)
"Given a string, STR, deal with anything that might cause processing problems."
(with-temp-buffer
(insert str)
(goto-char (point-min))
(let (pos)
(while (re-search-forward "\\[\\[" nil t)
(forward-char -2)
(setq pos (point))
(when (re-search-forward "\\]\\[" nil t)
(delete-region pos (point)))
(when (re-search-forward "\\]\\]" nil t)
(setq pos (point))
(forward-char -2)
(delete-region (point) pos))))
(setq str (buffer-string)))
(setq str (orgn--replace-string-in-string "\\" "\\\\" str)) ; Escape any backslashes
(setq str (orgn--replace-string-in-string "\"" "\\\"" str)) ; Escape any quotes (this must be run after escaping backslashes)
str)
(defun orgn--fold-show-all ()
"Run the deprecated org-show-all when Org version is less than 9.6.
Otherwise, run org-fold-show-all."
(unless orgn--org-version-checked-p
(if (string-version-lessp (org-version) "9.6")
(setq orgn--org-9.6-or-above-p nil)
(setq orgn--org-9.6-or-above-p t))
(setq orgn--org-version-checked-p t))
(if orgn--org-9.6-or-above-p
(org-fold-show-all)
(org-show-all)))
(defun orgn--format-time-string (format-string &optional time-zone)
"Run the deprecated `org-format-time-string' when Org version is less than 9.6.
Otherwise, run `format-time-string'.
FORMAT-STRING is the output format.
TIME-ZONE is the given time. If omitted or nil, use local time."
(unless orgn--org-version-checked-p
(if (string-version-lessp (org-version) "9.6")
(setq orgn--org-9.6-or-above-p nil)
(setq orgn--org-9.6-or-above-p t))
(setq orgn--org-version-checked-p t))
(if orgn--org-9.6-or-above-p
(format-time-string format-string time-zone)
(org-format-time-string format-string time-zone)))
(defun orgn--delete-line ()
"If Emacs version is less than 29, delete line the old fashioned way."
(let ((inhibit-field-text-motion t))
(unless orgn--emacs-version-checked-p
(if (>= (string-to-number (nth 0 (split-string (string-trim-left (emacs-version) "GNU Emacs ") "\\."))) 29)
(setq orgn--emacs-29-or-above-p t)
(setq orgn--emacs-29-or-above-p nil))
(setq orgn--emacs-version-checked-p t))
(if orgn--emacs-29-or-above-p
(delete-line)
(delete-region (line-beginning-position) (line-beginning-position 2)))))
;;;; File Manipulation Worker Functions
(defun orgn--string-to-file (str filename)
"Create/Overwrite FILENAME with the contents of STR."
(catch 'FILE-NOT-WRITABLE
(if (get-file-buffer filename)
;; Filename already open in a buffer. Update buffer and save.
(with-current-buffer (get-file-buffer filename)
(erase-buffer)
(insert str)
(save-buffer) ; Calling `save-buffer' with an argument of 0 would stop back-up files being created, but it's probably best to respect the user's Emacs setup in this regard
(when (or (equal 'org-novelist-mode major-mode) (equal 'org-mode major-mode))
(org-update-radio-target-regexp)))
;; Filename not open in a buffer. Just deal with file.
(with-temp-buffer
(insert str)
;; If directory doesn't exist, create it.
(unless (file-exists-p (file-name-directory filename))
(make-directory (file-name-directory filename) t))
(if (file-writable-p filename)
(write-region (point-min) (point-max) filename)
(progn
(error (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") filename (orgn--ls "filename-is-not-writable")))
(throw 'FILE-NOT-WRITABLE (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") filename (orgn--ls "filename-is-not-writable")))))))))
(defun orgn--get-relative-path (absolute-file-name current-folder)
"Given an ABSOLUTE-FILE-NAME and CURRENT-FOLDER, return relative path."
(let ((abs-file-dir-list (split-string (expand-file-name (file-name-directory (directory-file-name absolute-file-name))) / t " "))
(curr-folder-dir-list (split-string (expand-file-name (file-name-directory (file-name-as-directory current-folder))) / t " "))
curr-dir
(up-dir-fragment "")
(relative-path ""))
(while curr-folder-dir-list
(setq curr-dir (pop curr-folder-dir-list))
(unless (string= curr-dir (car abs-file-dir-list))
(setq up-dir-fragment (concat up-dir-fragment ".." /)))
(when (car (last curr-folder-dir-list))
(setq abs-file-dir-list (remove curr-dir abs-file-dir-list))))
(if (string= curr-dir (car abs-file-dir-list))
(setq relative-path (concat "." / (string-join (cdr abs-file-dir-list) /) / (file-name-nondirectory absolute-file-name)))
(setq relative-path (concat up-dir-fragment (string-join abs-file-dir-list /) / (file-name-nondirectory absolute-file-name))))
relative-path))
(defun orgn--generate-file-from-template (substitutions template filename)
"Generate a new file from TEMPLATE string and SUBSTITUTIONS hash table.
The new file, FILENAME, will be saved to disk."
(orgn--string-to-file (orgn--generate-string-from-template substitutions template) filename))
(defun orgn--generate-string-from-template (substitutions template &optional fixedcase)
"Generate a new string from TEMPLATE string and SUBSTITUTIONS hash table.
If FIXEDCASE is non-nil, do not alter the case of the replacement text."
(let ((keys (hash-table-keys substitutions))
key)
(while keys
(setq key (pop keys))
(setq template (orgn--replace-string-in-string key (gethash key substitutions) template fixedcase)))
template))
(defun orgn--replace-true-headline-in-org-heading (new-headline org-heading-components &optional new-level)
"Given an ORG-HEADING-COMPONENTS, replace the true headline with NEW-HEADLINE.
If integer NEW-LEVEL is specified, change heading to that level."
(let* ((stars (nth 1 org-heading-components))
(stars-str "")
(todo-str (nth 2 org-heading-components))
(priority-str nil)
;; (true-headline-str (nth 4 org-heading-components))
(tags-str (nth 5 org-heading-components))
(output-str ""))
(when new-level
(setq stars new-level))
(while (> stars 0)
(setq stars (- stars 1))
(setq stars-str (concat stars-str "*")))
(when (nth 3 org-heading-components)
(setq priority-str (concat "[#" (char-to-string (nth 3 org-heading-components)) "]")))
(setq output-str stars-str)
(when todo-str
(setq output-str (concat output-str " " todo-str)))
(when priority-str
(setq output-str (concat output-str " " priority-str)))
(setq output-str (concat output-str " " new-headline))
(when tags-str
(setq output-str (concat output-str " " tags-str)))
output-str))
(defun orgn--story-root-folder (&optional folder)
"Return the Org Novelist story's root folder.
If no FOLDER string is supplied, use the current buffer's file location. If
the given folder or current buffer is not part of an Org Novelist story folder,
throw an error."
(catch 'NOT-A-STORY-FOLDER
(let (current-folder)
(if (string= " *temp*" (buffer-file-name))
(eval nil)
(progn
(unless folder
(if (or load-file-name buffer-file-name)
(setq folder (directory-file-name (file-name-directory (or load-file-name buffer-file-name))))
(progn
(user-error (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") (orgn--ls "unsaved-buffer") (orgn--ls "filename-is-not-part-of-a-story-folder")))
(throw 'NOT-A-STORY-FOLDER (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") (orgn--ls "unsaved-buffer") (orgn--ls "filename-is-not-part-of-a-story-folder"))))))
(setq current-folder folder)
(while (not (file-exists-p (concat current-folder / orgn--config-filename)))
(when (string= current-folder (setq current-folder (expand-file-name (concat folder / ".." ))))
(user-error (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") folder (orgn--ls "filename-is-not-part-of-a-story-folder")))
(throw 'NOT-A-STORY-FOLDER (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") folder (orgn--ls "filename-is-not-part-of-a-story-folder")))))
(expand-file-name current-folder))))))
(defun orgn--story-name (&optional story-folder)
"Return the Org Novelist story's name.
If no STORY-FOLDER is supplied, try to determine the name for Org Novelist story
related to the current buffer."
(catch 'CONFIG-MISSING
(if (not story-folder)
(setq story-folder (orgn--story-root-folder))
(setq story-folder (orgn--story-root-folder story-folder)))
(let ((story-language-tag (orgn--get-file-property-value orgn--language-tag-property (concat (expand-file-name story-folder) / orgn--data-filename)))
story-name)
(when (string= "" story-language-tag)
(setq story-language-tag "en-GB"))
(with-temp-buffer
(if (file-exists-p (concat story-folder / (orgn--fls "main-file" story-language-tag orgn--file-ending)))
(if (file-readable-p (concat story-folder / (orgn--fls "main-file" story-language-tag orgn--file-ending)))
(progn
(insert-file-contents (concat story-folder / (orgn--fls "main-file" story-language-tag orgn--file-ending)))
(goto-char (point-min)) ; Move point to start of buffer
(org-novelist-mode) ; If not explicitly in Org mode (or a derivative), then org-heading-components won't work in the temp buffer
(orgn--fold-show-all) ; Belts and braces
(if (org-goto-first-child) ; Get the first heading in buffer
(setq story-name (nth 4 (org-heading-components))) ; Extract just the heading text without other Org stuff
(progn
(error (orgn--ls "no-story-found"))
(throw 'CONFIG-MISSING (orgn--ls "no-story-found")))))
(progn
(error (orgn--replace-string-in-string (concat "<<" (orgn--fls "filename" story-language-tag) ">>") (orgn--fls "main-file" story-language-tag orgn--file-ending) (orgn--fls "filename-is-not-readable" story-language-tag)))
(throw 'CONFIG-MISSING (orgn--replace-string-in-string (concat "<<" (orgn--fls "filename" story-language-tag) ">>") (orgn--fls "main-file" story-language-tag orgn--file-ending) (orgn--fls "filename-is-not-readable" story-language-tag)))))
(progn
(error (orgn--ls "no-story-found"))
(throw 'CONFIG-MISSING (orgn--ls "no-story-found")))))
story-name)))
(defun orgn--set-story-name (new-story-name &optional story-folder)
"Set the Org Novelist story's name in the main entry point file.
NEW-STORY-NAME will be used as the new story name.
If no STORY-FOLDER is supplied, try to determine the name for Org Novelist story
related to the current buffer."
(if (not story-folder)
(setq story-folder (orgn--story-root-folder))
(setq story-folder (orgn--story-root-folder story-folder)))
(catch 'SET-STORY-NAME-FAILURE
(let (org-heading-components
beg)
(with-temp-buffer
(if (file-exists-p (concat story-folder / (orgn--ls "main-file" orgn--file-ending)))
(if (file-readable-p (concat story-folder / (orgn--ls "main-file" orgn--file-ending)))
(progn
(insert-file-contents (concat story-folder / (orgn--ls "main-file" orgn--file-ending)))
(goto-char (point-min)) ; Move point to start of buffer
(org-novelist-mode) ; If not explicitly in Org mode (or a derivative), then org-heading-components won't work in the temp buffer
(orgn--fold-show-all) ; Belts and braces
(if (org-goto-first-child) ; Get the first heading in buffer
(progn
(setq org-heading-components (org-heading-components)) ; Extract heading components
(beginning-of-line)
(setq beg (point))
(end-of-line)
(delete-region beg (point))
(insert (orgn--replace-true-headline-in-org-heading new-story-name org-heading-components))
(orgn--string-to-file (buffer-string) (concat story-folder / (orgn--ls "main-file" orgn--file-ending))))
(progn
(error (orgn--ls "no-story-found"))
(throw 'SET-STORY-NAME-FAILURE (orgn--ls "no-story-found")))))
(progn
(error (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") (orgn--ls "main-file" orgn--file-ending) (orgn--ls "filename-is-not-readable")))
(throw 'SET-STORY-NAME-FAILURE (orgn--replace-string-in-string (concat "<<" (orgn--ls "filename") ">>") (orgn--ls "main-file" orgn--file-ending) (orgn--ls "filename-is-not-readable")))))
(progn
(error (orgn--ls "no-story-found"))
(throw 'SET-STORY-NAME-FAILURE (orgn--ls "no-story-found"))))))))
(defun orgn--delete-current-file (&optional no-prompt)
"Delete the file associated with the current buffer.
Kill the current buffer too. If no file is associated, just kill buffer without
prompt for save. If NO-PROMPT is non-nil, don't ask user for confirmation."
(let ((current-file (buffer-file-name)))
(if no-prompt
(progn
(set-buffer-modified-p nil)
(kill-buffer (current-buffer))
(when current-file
(delete-file current-file)))
(when (yes-or-no-p (concat (orgn--ls "delete-file-query") " " current-file " "))
(kill-buffer (current-buffer))
(when current-file
(delete-file current-file))))))
(defun orgn--rename-current-file (new-file &optional no-prompt)
"Rename the file associated with the current buffer to NEW-FILE.
If no file is associated, inform the user. If NO-PROMPT is non-nil, don't ask
user for confirmation if new file name already in use."
(let ((current-file (buffer-file-name)))
(when current-file
(save-buffer)
(if no-prompt
(progn
(rename-file current-file new-file t))
(progn
(rename-file current-file new-file 1)))
(kill-buffer (current-buffer))
(find-file new-file))))
(defun orgn--save-current-file (&optional file)
"Save the current buffer to its associated file.
If no file associated with current buffer, do nothing.
If passed a FILE, see if their is a matching buffer and save it."
(if (and file (get-file-buffer file))
(with-current-buffer (get-file-buffer file)
(save-buffer))
(when (buffer-file-name)
(save-buffer))))
(defun orgn--make-chapter-at-index-point (chapter-name)
"Create a new chapter file and link to it from the current point.
CHAPTER-NAME should be the name of the chapter. The new chapter file will also
have a dedicated notes file linked from it."
(let* ((story-folder (orgn--story-root-folder)) ; Get the story's root directory for the buffer being displayed when this function is called. This also ensures we're in a story folder.
(chapter-file (concat (orgn--ls "chapter-file-prefix") (orgn--system-safe-name chapter-name)))
(story-name (orgn--story-name story-folder)))
(insert (format "\[\[file:../%s/%s\]\[%s\]\]" (orgn--ls "chapters-folder") (concat chapter-file orgn--file-ending) chapter-name))
(orgn--populate-chapter-template story-name story-folder chapter-file chapter-name)
(orgn--populate-chapter-notes-template story-name story-folder chapter-file chapter-name)))
(defun orgn--make-character-at-index-point (character-name)
"Create a new character file and link to it from the current point.
CHARACTER-NAME should be the name of the character."
(let* ((story-folder (orgn--story-root-folder)) ; Get the story's root directory for the buffer being displayed when this function is called. This also ensures we're in a story folder.
(character-file (concat (orgn--ls "character-file-prefix") (orgn--system-safe-name character-name)))
(story-name (orgn--story-name story-folder)))
(insert (format "\[\[file:../%s/%s\]\[%s\]\]" (orgn--ls "notes-folder") (concat character-file orgn--file-ending) character-name))
(orgn--populate-character-notes-template story-name story-folder character-file character-name)))
(defun orgn--make-prop-at-index-point (prop-name)
"Create a new prop file and link to it from the current point.
PROP-NAME should be the name of the prop."
(let* ((story-folder (orgn--story-root-folder)) ; Get the story's root directory for the buffer being displayed when this function is called. This also ensures we're in a story folder.
(prop-file (concat (orgn--ls "prop-file-prefix") (orgn--system-safe-name prop-name)))
(story-name (orgn--story-name story-folder)))
(insert (format "\[\[file:../%s/%s\]\[%s\]\]" (orgn--ls "notes-folder") (concat prop-file orgn--file-ending) prop-name))
(orgn--populate-prop-notes-template story-name story-folder prop-file prop-name)))
(defun orgn--make-place-at-index-point (place-name)
"Create a new place file and link to it from the current point.
PLACE-NAME should be the name of the place."
(let* ((story-folder (orgn--story-root-folder)) ; Get the story's root directory for the buffer being displayed when this function is called. This also ensures we're in a story folder.
(place-file (concat (orgn--ls "place-file-prefix") (orgn--system-safe-name place-name)))
(story-name (orgn--story-name story-folder)))
(insert (format "\[\[file:../%s/%s\]\[%s\]\]" (orgn--ls "notes-folder") (concat place-file orgn--file-ending) place-name))
(orgn--populate-place-notes-template story-name story-folder place-file place-name)))
(defun orgn--make-glossary-string (story-folder)
"Create a new glossary for STORY-FOLDER."
(setq story-folder (orgn--story-root-folder story-folder))
(when story-folder
(orgn--populate-glossary-string story-folder)))
(defun orgn--make-export-glossary-string (story-folder &optional file-restriction)
"Create an export glossary without header for STORY-FOLDER.
If a file in passed as FILE-RESTRICTION, restrict glossary string to terms
that appear in that file."
(setq story-folder (orgn--story-root-folder story-folder))
(when story-folder
(catch 'EXPORT-GLOSSARY-STRING-FAULT
(let* ((story-pool (orgn--map-story-pool story-folder))
(file-characters (orgn--character-hash-table story-pool))
(file-places (orgn--place-hash-table story-pool))
(file-props (orgn--prop-hash-table story-pool))
(keys (sort (append (hash-table-keys file-characters)
(hash-table-keys file-places)
(hash-table-keys file-props)) 'string<))
key
aliases
alias
alias-words