-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcommands.py
473 lines (369 loc) · 17.1 KB
/
commands.py
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
import sublime
import sublime_plugin # noqa
import re
from .source.prose_utils import (camel_case, collapse_whitespace,
pluck_out_match, snake_case, transpose,
generate_valid_anchor_token)
from .source.asciidoc_utils import (classify_adoc_syntax, remove_links,
auto_align_table_columns, extract_headings,
opinionated_adoc_fixup, unwrap_paragraphs,
asciidoctor_syntax_fixup, index_tag)
from .source.cookbook_utils import (recipe_fixup)
from .source.journal_utils import (format_long, format_slug_with_time,
standardize_keywords, journal_entry)
from .source.manuscript_utils import (adoc_renumber_chapters, fix_scene_breaks,
quote_notate)
from .source.abstract_command import AbstractUtilTextCommand
# ###########################################################################
# Static Table of Contents
# ###########################################################################
class StaticTableOfContentsCommand(AbstractUtilTextCommand):
"""
Inserts, at the top of the buffer, an unordered list corresponding
to every heading in the current document.
"""
def _run(self):
contents = extract_headings(self.get_file_content().splitlines())
self.unselect("SOF")
self.replace_selected_text(sublime.Region(0, 0), "\n".join(contents) + "\n\n")
# ###########################################################################
# Renumber Chapters
# ###########################################################################
class AsciidocRenumberChaptersCommand(AbstractUtilTextCommand):
"""
Renumbers all chapter headings.
For example, if you have
...
== Chapter Nine: Saving the Cat
== Chapter Ten: The Mirror Moment
== Chapter Eleven: Hero Status Achieved
...
and you insert a new chapter between Ten and Eleven (== Chapter X: A Stumbling Block)
then renumbering will get you:
...
== Chapter Nine: Saving the Cat
== Chapter Ten: The Mirror Moment
== Chapter Eleven: A Stumbling Block
== Chapter Twelve: Hero Status Achieved
...
It also works for all of the following formats:
== Chapter 9: Saving the Cat
== Nine: Saving the Cat
== 9: Saving the Cat
== Chapter 9
== Nine
== 9
Whatever format it finds, it will replicate.
"""
def _run(self):
self.process_whole_file(adoc_renumber_chapters)
# ###########################################################################
# Select All Spelling Errors
# ###########################################################################
class SelectAllSpellingErrorsCommand(sublime_plugin.TextCommand):
"""
Clears any current selections, and then selects every spelling error in the
current document. Useful for analyzing commonly misspelled words,
made-up character names, etc.
"""
def run(self, edit):
regions = []
while True:
self.view.run_command('next_misspelling')
mispell = self.view.sel()[0]
# FIXME -- what if there aren't any misspellings?
if mispell in regions:
break
# FIXME -- why line start?
if self.view.classify(mispell.a) & sublime.CLASS_LINE_START:
regions.append(mispell)
self.view.sel().clear()
self.view.sel().add_all(regions)
# ###########################################################################
# Select Section
# ###########################################################################
class AsciidocSelectSection(AbstractUtilTextCommand):
"""
Expands the selection to encompass the entire chapter, section, or subsection.
That is, the selection is first expanded upwards until it hits a heading line
(= Title, == Chapter, === Section,etc.). Any preamble to the heading line,
such as anchors and comments, are also included.
The selection is then expanded downwards until it hits a corresponding
heading at the same level, or one at a more significant level, or the end
of the file.
Works with multi-select.
"""
def run(self, edit):
self.expand_selected_text_to_whole_subsection(classify_adoc_syntax)
# ###########################################################################
# Fixup
# ###########################################################################
class AsciidocProseFixupCommand(AbstractUtilTextCommand):
"""
Cleans up a document that has been converted to AsciiDoc, e.g. by PanDoc...
pandoc --from=docx --to=asciidoc --wrap=none --atx-headers --extract-media=extracted-media $FILENAME.docx > $FILENAME..adoc
"""
def _run(self):
if self.nothing_selected():
self.select_whole_file()
self.process_all_regions(opinionated_adoc_fixup)
class AsciidocUpdateSyntaxCommand(AbstractUtilTextCommand):
"""
Cleans up old-style AsciiDoc syntax to the AsciiDoctor flavor.
* Changes underlined title/headings to = prefixes
* Changes `` '' quotations to "` `"
"""
def _run(self):
if self.nothing_selected():
self.select_whole_file()
self.process_all_regions(asciidoctor_syntax_fixup)
# ###########################################################################
# Recipe Standardizer
# ###########################################################################
class RecipeStandardizerCommand(AbstractUtilTextCommand):
"""
Adjusts a recipe that was copied and pasted from elsewhere to be more AsciiDoc friendly.
"""
def _run(self):
if self.nothing_selected():
self.select_whole_file()
self.process_all_regions(recipe_fixup)
# ###########################################################################
# Scene-Breaks
# ###########################################################################
class AsciidocSceneBreakFixupCommand(AbstractUtilTextCommand):
"""
Converts all lines that look like a scene break to a horizontal rule (''')
"""
def _run(self):
self.process_all_regions(fix_scene_breaks)
# ###########################################################################
# Quote Notation
# ###########################################################################
class AsciidocQuoteNotationCommand(AbstractUtilTextCommand):
"""
Converts the selection to a Quote Block. If it finds one or more m-dashes
or tildes, it assumes that the attribution follows.
"""
def _run(self):
if self.nothing_selected():
self.expand_selected_text_to_whole_lines()
self.process_all_regions(quote_notate)
# ###########################################################################
# Align Table
# ###########################################################################
class AsciidocAlignTable(AbstractUtilTextCommand):
"""
Adjusts the spacing within the lines of a table so that the pipe separators
all line up.
"""
def _run(self):
self.process_all_regions(auto_align_table_columns)
# ###########################################################################
# Snake Case
# ###########################################################################
class SnakeCaseCommand(AbstractUtilTextCommand):
def _run(self):
self.process_all_regions(snake_case)
class CamelCaseCommand(AbstractUtilTextCommand):
def _run(self):
self.process_all_regions(camel_case)
# ###########################################################################
# Transpose
# ###########################################################################
class TransposeCommand(AbstractUtilTextCommand):
def _run(self):
self.process_all_regions(transpose)
# ###########################################################################
# Index Tag
# ###########################################################################
class IndexTagCommand(AbstractUtilTextCommand):
def _run(self):
self.process_all_regions(index_tag)
# ###########################################################################
# Link/Unlink
# ###########################################################################
class AsciidocLinkify(sublime_plugin.TextCommand):
"""
Coverts the selected text to an AsciiDoc link.
"Apple Pie" -> <<apple-pie,Apple Pie>>
Works with multiple selections.
"""
def run(self, edit):
self._edit = edit
for i, region in enumerate(self.view.sel()):
link_text = self.view.substr(region).strip()
if len(link_text) == 0:
continue
# TODO expand selection to word
block_id = generate_valid_anchor_token(link_text)
self.view.replace(self._edit, region, f"<<{block_id},{link_text}>>")
class AsciidocAnchorify(sublime_plugin.TextCommand):
"""
Generates an AsciiDoc anchor according to the selected text.
"Apple Pie" -> [[apple-pie]] (on the line above).
Works with multiple selections.
"""
def run(self, edit):
self._edit = edit
for i, region in enumerate(self.view.sel()):
link_text = self.view.substr(region).strip()
if len(link_text) == 0:
continue
# TODO expand selection to word
whole_line_region = self.view.line(region)
whole_line_text = self.view.substr(whole_line_region)
block_id = generate_valid_anchor_token(link_text)
self.view.replace(self._edit, whole_line_region, f'[[{block_id}]]\n{whole_line_text}')
class RemoveLinksCommand(AbstractUtilTextCommand):
def _run(self):
self.process_all_regions(remove_links)
# ###########################################################################
# Unwrap Paragraphs
# ###########################################################################
class UnwrapParagraphsCommand(AbstractUtilTextCommand):
"""
In AsciiDoc, paragraphs are represented by text that (can) span multiple
lines, and one or more blank lines designates the break between paragraphs.
This command undoes that, combining the text of each paragraph onto a
single line, leaving no blank lines.
"""
def _run(self):
self.expand_selected_text_to_whole_lines()
self.process_all_regions(unwrap_paragraphs)
# ###########################################################################
# Convert From RST
# ###########################################################################
class AsciidocFromRstCommand(AbstractUtilTextCommand):
"""
A quick-and-dirty start to converting RST markup to AsciiDoc
"""
FOOTNOTE_PATTERN = r"^\.\. +_([^:]*): +(https?://[^\[\s]*)"
VARIABLE_PATTERN = r"^\.\. +\|([^\|]*)\| +replace:: +(.*)$"
def _run(self):
self.expand_selected_text_to_whole_lines()
self.process_all_regions(self.from_rst)
def from_rst(self, txt: str) -> str:
txt = collapse_whitespace(txt)
# Gather footnoted http links
footnotes = {}
rst_footnotes = re.findall(self.FOOTNOTE_PATTERN, txt, flags=re.MULTILINE)
for rst_footnote in rst_footnotes:
ref, link = rst_footnote
footnotes[ref] = link
txt = re.sub(self.FOOTNOTE_PATTERN, "", txt, flags=re.MULTILINE)
# Gather variables
variables = {}
rst_variables = re.findall(self.VARIABLE_PATTERN, txt, flags=re.MULTILINE)
for rst_variable in rst_variables:
name, definition = rst_variable
variables[name] = definition
txt = re.sub(self.VARIABLE_PATTERN, "", txt, flags=re.MULTILINE)
# --- The following conversions can be done en masse ---
# Monospaced
txt = re.sub(r"``(.*?)``", '`\\1`', txt)
# Anchor
txt = re.sub(r"^\.\. +\[#(\w+)\] ", "[[\\1]]\n", txt, flags=re.MULTILINE)
# Link to anchor
txt = re.sub(r"`(.*?)`_ \[#(.*?)\]_", "<<\\2,\\1>>", txt)
# Variable references
while True:
m = re.search(r"\|(.*?)\|", txt)
if m:
ref = m.group(1).replace("\n", " ")
if ref in variables:
proper_ref = ref.replace(" ", "_")
txt = pluck_out_match(txt, m, "{" + proper_ref + "}")
else:
txt = pluck_out_match(txt, m, ref)
else:
break
# Link to footnoted http link
while True:
m = re.search(r"`([^`]*?)`_", txt)
if m:
ref = m[1].replace("\n", " ")
if ref in footnotes:
txt = pluck_out_match(txt, m, footnotes[ref] + "[" + ref + "]")
else:
txt = pluck_out_match(txt, m, ref)
else:
break
while True:
m = re.search(r"\b(\w*)_\b", txt)
if m:
ref = m.group(1)
if ref in footnotes:
txt = pluck_out_match(txt, m, footnotes[ref] + "[" + ref + "]")
else:
txt = pluck_out_match(txt, m, ref)
else:
break
# TODO Comments
txt = re.sub(r"^\.\. +todo::", "// TODO ", txt, flags=re.MULTILINE)
# Comments
txt = re.sub(r"^\.\. +<--(.*?)-->", "// \\1 ", txt, flags=re.MULTILINE)
# Ordered lists
txt = re.sub(r"^#\. ", ". ", txt, flags=re.MULTILINE)
# Footnoted http link
txt = re.sub(r"^\.\. +_([^:]*): +(https?://[^\[\s]*)", "[[\\1]]\n\\2[\\1]", txt, flags=re.MULTILINE)
# --- The following conversions need to be done line-by-line ---
lines = txt.splitlines()
current_anchor = ""
seeking_byline = False
for i, line in enumerate(lines):
# H2
m = re.match(r"^(=+)$", line)
if m and i > 0:
previous_line = lines[i - 1]
if len(previous_line) == len(line):
lines[i - 1] = "== " + previous_line
lines[i] = ""
# H3
m = re.match(r"^(-+)$", line)
if m and i > 0:
previous_line = lines[i - 1]
if len(previous_line) == len(line):
lines[i - 1] = "=== " + previous_line
lines[i] = ""
txt = "\n".join(lines)
# --- More conversions that can be done en masse ---
# Double-colons
txt = re.sub(r":: *$", ":", txt, flags=re.MULTILINE)
if variables:
txt = "\n".join([":{}: {}".format(ref.replace(" ", "_"), variables[ref]) for ref in variables]) + "\n\n" + txt
return txt
# ###########################################################################
# Journal Entry
# ###########################################################################
class JournalEntryCommand(AbstractUtilTextCommand):
def _run(self, **kwargs):
self.expand_selected_text_to_whole_lines()
self.process_all_regions(journal_entry, as_snippet=True, unselect_after=True, **kwargs)
# ###########################################################################
# Dropped Images
# ###########################################################################
class AsciidocGatherDroppedImagesCommand(sublime_plugin.TextCommand):
def run(self, edit):
self._edit = edit
filenames = self._gather_images()
results = [f"image::{f}[]" for f in filenames]
self._replace_selected_text(results)
self._unselect()
def _replace_selected_text(self, doc):
if type(doc) is list:
doc = "\n".join(doc)
doc += "\n"
self.view.replace(self._edit, self.view.sel()[0], doc)
def _unselect(self):
s = self.view.sel()
pt = s[0].end()
s.clear()
s.add(sublime.Region(pt, pt))
def _gather_images(self) -> list:
results = []
for sheet in self.view.window().sheets():
if (type(sheet) is sublime.ImageSheet):
results.append(sheet.file_name())
sheet.close()
return results