Skip to content

Commit

Permalink
add: support for rstudio foldable sections
Browse files Browse the repository at this point in the history
test: added check for missing reference file
  • Loading branch information
AlphaJack committed Jan 29, 2024
1 parent 097d908 commit 0783fe7
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 43 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,3 @@ If you have a suggestion or you found a bug, you can use GitHub [issues](https:/

See [CHANGELOG.md](./CHANGELOG.md)

[^1]: No, not really, it's just a match-case statement using the file extension, defaulting to "#"
[^2]: Not even, it's just a bunch of if-else and try-excepts statement that may prevent catastrophic damage
[^3]: The outdated toc to be replaced is defined as the the first match of a non-greedy regex
107 changes: 87 additions & 20 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,50 @@
// │ │ ├── Markdown
// │ │ ├── Beancount
// │ │ └── Perl
// │ └──┐Wrap around comments needed
// │ ├── CSS
// │ ├── HTML
// │ └── OCaml
// │ ├──┐Wrap around comments needed
// │ │ ├── CSS
// │ │ ├── HTML and Quarto
// │ │ └── OCaml
// │ └──┐Compatibility with third-party editors
// │ ├── Vim and Emacs
// │ └── RStudio
// ├── ################################################################ First section ----
// ├── text 1
// ├── ################################ Second section ####
// ├── text 2
// ├── ################################ Third section ====
// │
// └───────────────────────────────────────────────────────────────
-->

# Detailed TOC usage

The scenarios below show different features.
The scenarios below show different features of `toc`

<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->

- [Detailed TOC usage](#detailed-toc-usage)
- [Read the table of contents](#read-the-table-of-contents)
- [Embed the table of contents in the original file](#embed-the-table-of-contents-in-the-original-file)
- [Set a custom comment character](#set-a-custom-comment-character)
- [Show line numbers](#show-line-numbers)
- [Process multiple files](#process-multiple-files)
- [Exceptional file types](#exceptional-file-types)
- [No comments needed](#no-comments-needed)
* [Read the table of contents](#read-the-table-of-contents)
* [Embed the table of contents in the original file](#embed-the-table-of-contents-in-the-original-file)
* [Process multiple files](#process-multiple-files)
* [Show line numbers](#show-line-numbers)
* [Set a custom comment character](#set-a-custom-comment-character)
* [Redirect output to another file](#redirect-output-to-another-file)
* [Other commands](#other-commands)
* [Exceptional file types](#exceptional-file-types)
+ [No comments needed](#no-comments-needed)
- [Markdown](#markdown)
- [Beancount](#beancount)
- [Perl](#perl)
- [Wrap around comments needed](#wrap-around-comments-needed)
+ [Wrap around comments needed](#wrap-around-comments-needed)
- [CSS](#css)
- [HTML](#html)
- [HTML and Quarto](#html-and-quarto)
- [OCaml](#ocaml)
+ [Compatibility with third-party editors](#compatibility-with-third-party-editors)
- [Vim and Emacs](#vim-and-emacs)
- [RStudio](#rstudio)

<!-- TOC end -->

## Read the table of contents

Expand Down Expand Up @@ -79,7 +96,17 @@ let Section5 = "Write //, 4 hash characters and the name of section"
If you run `toc example.js`, the program will output the following (stdout):

```js

// ┌───────────────────────────────────────────────────────────────┐
// │ Contents of example.js │
// ├───────────────────────────────────────────────────────────────┘
//
// ├──┐Main section
// │ └──┐Nested section
// │ └──┐Nested section
// │ └──┐Nested section
// │ └── Nested section
//
// └───────────────────────────────────────────────────────────────
```

## Embed the table of contents in the original file
Expand Down Expand Up @@ -252,7 +279,7 @@ If you feel brave enough, you can run `toc *` over your entire code base, as its

For very long files, it may come in handy to run `toc -n example.js` to see the line number of each section, similar to the page numbers in the table of contents of a book:

```
```js
// ┌───────────────────────────────────────────────────────────────┐
// │ Contents of example.js │
// ├───────────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -303,10 +330,10 @@ For Markdown files, you don't need to write comments, just organize your section

For [Beancount](https://raw.githubusercontent.com/beancount/beancount/master/examples/example.beancount) files, it's the same for Markdown, but you use `*` instead:

```beancount
```ini
* Options

; text
; comment

* Transactions
** FY2020
Expand Down Expand Up @@ -370,9 +397,9 @@ For CSS files, you have to wrap your `//` comments between `/*` and `*/`:

</details>

#### HTML
#### HTML and Quarto

For HTML files, you have to wrap your `//` comments between `<!--` and `-->`:
For HTML and Quarto files, you have to wrap your `//` comments between `<!--` and `-->`:

<details>
<summary>Click to view `example.html`</summary>
Expand Down Expand Up @@ -418,3 +445,43 @@ let () = print_endline "Hello, World!"
```

</details>


### Compatibility with third-party editors

#### Vim and Emacs

If you place your Vim Modeline / Emacs mode as the first line, the toc will be appended after


#### RStudio

If you are using RStudio, you may want to end your comments with at least 4 `-`, `=` or `#`.
This marks the comment as a foldable sections:

<details>
<summary>Click to view `example.R`</summary>

```r
# ################################################################ Foldable section 1 ----

print("Collapse me!")

# ################################ Foldable section 2 ####

print("Collapse me!")

# ################################ Foldable section 3 ====

print("Collapse me!")
```

</details>





[^1]: No, not really, it's just a match-case statement using the file extension, defaulting to "#"
[^2]: Not even, it's just a bunch of if-else and try-excepts statement that may prevent catastrophic damage
[^3]: The outdated toc to be replaced is defined as the the first match of a non-greedy regex
9 changes: 9 additions & 0 deletions tests/input/bash_vim_modeline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# vim:set ts=4 sw=4 ft=sh et:

# ################################################################ Nested 1
# ################################ Nested 2
# ################ Nested 3
# ######## Nested 4
# #### Nested 5
# ## Nested 6
echo "OK"
4 changes: 0 additions & 4 deletions tests/input/markdown_empty_first_line.md

This file was deleted.

16 changes: 16 additions & 0 deletions tests/input/r_simple.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ################################################################ Head 1 ####

# test

# ################################ Head 2 ====

# test

# ################ Head 3 ----

# test

## ################ Invalid ----


#
22 changes: 22 additions & 0 deletions tests/reference/bash_vim_modeline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# vim:set ts=4 sw=4 ft=sh et:

# ┌───────────────────────────────────────────────────────────────┐
# │ Contents of bash_vim_modeline.sh │
# ├───────────────────────────────────────────────────────────────┘
#
# ├──┐Nested 1
# │ └──┐Nested 2
# │ └──┐Nested 3
# │ └──┐Nested 4
# │ └──┐Nested 5
# │ └── Nested 6
#
# └───────────────────────────────────────────────────────────────

# ################################################################ Nested 1
# ################################ Nested 2
# ################ Nested 3
# ######## Nested 4
# #### Nested 5
# ## Nested 6
echo "OK"
File renamed without changes.
26 changes: 26 additions & 0 deletions tests/reference/r_simple.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# ┌───────────────────────────────────────────────────────────────┐
# │ Contents of r_simple.R │
# ├───────────────────────────────────────────────────────────────┘
#
# ├──┐Head 1
# │ └──┐Head 2
# │ └── Head 3
#
# └───────────────────────────────────────────────────────────────

# ################################################################ Head 1 ####

# test

# ################################ Head 2 ====

# test

# ################ Head 3 ----

# test

## ################ Invalid ----


#
10 changes: 7 additions & 3 deletions tests/test_toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,19 @@ def test_input_files(self):
output_content, reference_content = output.read(), reference.read()
comparison = True if output_content == reference_content else False
with self.subTest(comparison=comparison):
self.assertTrue(comparison, f'Unexpected output processing "{file.name}", please check "{self.output_dir}"')
self.assertTrue(comparison, f'Unexpected content (should be different) processing "{file.name}", please check "{self.output_dir}"')
elif output_file.is_file():
comparison = False
with self.subTest(comparison=comparison):
self.assertTrue(comparison, f'Unexpected output file (should be none) processing "{file.name}", please check "{self.output_dir}"')
elif reference_file.is_file():
comparison = False
with self.subTest(comparison=comparison):
self.assertTrue(comparison, f'Unexpected empty output processing "{file.name}", please check "{self.output_dir}"')
self.assertTrue(comparison, f'Unexpected empty output (should be something) processing "{file.name}", please check "{self.output_dir}"')
else:
comparison = True
with self.subTest(comparison=comparison):
self.assertTrue(comparison, f'Expected empty output processing "{file.name}"')
self.assertTrue(comparison, f'Expected empty output (ok) processing "{file.name}"')

# @classmethod
# def tearDownClass(cls):
Expand Down
36 changes: 23 additions & 13 deletions toc/toc.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def __init__(self, input: str = "", output=None, lineNumbers: bool = False, char
def set_character(self):
# automatically select the comment type from its extension, if not already set
match self.extension:
case "c" | "carbon" | "cc" | "coffee" | "cpp" | "cs" | "css" | "d" | "dart" | "go" | "h" | "hpp" | "htm" | "html" | "hxx" | "java" | "js" | "kt" | "md" | "pas" | "php" | "pp" | "proto" | "qs" | "rs" | "scala" | "sc" | "swift" | "ts" | "typ" | "xml" | "zig":
case "c" | "carbon" | "cc" | "coffee" | "cpp" | "cs" | "css" | "d" | "dart" | "go" | "h" | "hpp" | "htm" | "html" | "hxx" | "java" | "js" | "kt" | "md" | "pas" | "php" | "pp" | "proto" | "qmd" | "qs" | "rs" | "scala" | "sc" | "swift" | "ts" | "typ" | "xml" | "zig":
self.character = "//"
case "ahk" | "asm" | "beancount" | "cl" | "clj" | "cljs" | "cljc" | "edn" | "fasl" | "ini" | "lisp" | "lsp" | "rkt" | "scm" | "ss":
self.character = ";"
Expand Down Expand Up @@ -251,7 +251,7 @@ def _toc_header(self):
match self.extension:
case "css":
_tocPrefix = "/*"
case "html" | "md" | "xml":
case "html" | "xml" | "md" | "qmd" | "rmd":
_tocPrefix = "<!--"
case "ml" | "mli" | "scpd" | "scpt":
_tocPrefix = "(*"
Expand Down Expand Up @@ -319,16 +319,17 @@ def _toc_body(self):
def _process_beancount(self, lines):
# pars beancount files, reusing collapsible sections
_oldtoc, _newtoc = [], []
_pattern = re.compile(r"^\*+ ")
if self.lineNumbers:
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if re.search(r"^\*+ .*$", line)]
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if re.search(_pattern, line)]
else:
_oldtoc = [line for line in lines if re.search(r"^\*+ .*$", line)]
_oldtoc = [line for line in lines if re.search(_pattern, line)]
_newtoc = [re.sub(r"^\*{6}", "\t│ └──", line.strip()) for line in _oldtoc]
_newtoc = [re.sub(r"^\*{5}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^\*{4}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^\*{3}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^\*{2}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^\*", "\t├──", line) for line in _newtoc]
_newtoc = [re.sub(r"^\*{1}", "\t├──", line) for line in _newtoc]
_newtoc = [line.replace("\t", f"\n{self.character} ") for line in _newtoc]
return _newtoc

Expand All @@ -337,16 +338,17 @@ def _process_beancount(self, lines):
def _process_markdown(self, lines):
# pars markdown files, reusing headings
_oldtoc, _newtoc = [], []
_pattern = re.compile(r"^#+ ")
if self.lineNumbers:
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if re.search(r"^#+ ", line)] # [^#│├└┌]
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if re.search(_pattern, line)]
else:
_oldtoc = [line for line in lines if re.search(r"^#+ ", line)] # [^#│├└┌] # skip matching toc if "#" is used as character symbol of markdown
_oldtoc = [line for line in lines if re.search(_pattern, line)]
_newtoc = [re.sub(r"^#{6}", "\t│ └──", line.strip()) for line in _oldtoc]
_newtoc = [re.sub(r"^#{5}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^#{4}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^#{3}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^#{2}", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^#", "\t├──", line) for line in _newtoc]
_newtoc = [re.sub(r"^#{1}", "\t├──", line) for line in _newtoc]
_newtoc = [line.replace("\t", f"\n{self.character} ") for line in _newtoc]
return _newtoc

Expand All @@ -356,10 +358,11 @@ def _process_markdown(self, lines):
def _process_pod(self, lines):
# pars perl files, reusing headings
_oldtoc, _newtoc = [], []
_pattern = re.compile(r"^=head\d ")
if self.lineNumbers:
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if re.search(r"^=head\d .*$", line)]
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if re.search(_pattern, line)]
else:
_oldtoc = [line for line in lines if re.search(r"^=head\d .*$", line)]
_oldtoc = [line for line in lines if re.search(_pattern, line)]
_newtoc = [re.sub(r"^=head6", "\t│ └──", line.strip()) for line in _oldtoc]
_newtoc = [re.sub(r"^=head5", "\t│ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^=head4", "\t│ └──", line) for line in _newtoc]
Expand All @@ -374,15 +377,22 @@ def _process_pod(self, lines):
def _process_other(self, lines):
# parse all other files types, according to the comment convention
_oldtoc, _newtoc = [], []
_pattern = re.compile(r"^" + re.escape(self.character) + " (?:#{64}|#{32}|#{16}|#{8}|#{4}|#{2})")
# need to define _oldtoc here, otherwise it does not return the proper line number
if self.lineNumbers:
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if line.startswith(self.character) and "####" in line]
_oldtoc = [f"{line.strip()} {i+1}" for i, line in enumerate(lines) if re.search(_pattern, line)]
else:
_oldtoc = [line.strip() for line in lines if line.startswith(self.character) and "####" in line]
_oldtoc = [line.strip() for line in lines if re.search(_pattern, line)]
_newtoc = [re.sub(r"^" + re.escape(self.character) + " ################################################################", "\n" + self.character + " ├──", line) for line in _oldtoc]
_newtoc = [re.sub(r"^" + re.escape(self.character) + " ################################", "\n" + self.character + " │ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^" + re.escape(self.character) + " ################", "\n" + self.character + " │ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^" + re.escape(self.character) + " ########", "\n" + self.character + " │ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^" + re.escape(self.character) + " ####", "\n" + self.character + " │ └──", line) for line in _newtoc]
_newtoc = [re.sub(r"^" + re.escape(self.character) + " ##", "\n" + self.character + " │ └──", line) for line in _newtoc]
match self.extension:
# https://support.posit.co/hc/en-us/articles/200484568-Code-Folding-and-Sections-in-the-RStudio-IDE
case "r" | "rpres":
_newtoc = [re.sub(r" [#-=]{4,}", "", line) for line in _newtoc]
return _newtoc

# #### PRETTIFY CONNECTORS
Expand Down Expand Up @@ -436,7 +446,7 @@ def _toc_footer(self):
match self.extension:
case "css":
_tocSuffix = "*/"
case "html" | "md" | "xml":
case "html" | "xml" | "md" | "qmd" | "rmd":
_tocSuffix = "-->"
case "ml" | "mli" | "scpd" | "scpt":
_tocSuffix = "*)"
Expand Down

0 comments on commit 0783fe7

Please sign in to comment.