├── .python-version ├── Default.sublime-commands ├── LICENSE ├── OutlineNotesPublisher.py └── README.md /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Outline to HTML: Create from selection", 4 | "command": "outline_to_html" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /OutlineNotesPublisher.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import re 3 | 4 | """ 5 | Configure using Preferences ➡ Settings 6 | { 7 | // ... 8 | "outline_to_html": { 9 | // "css": "", // Override CSS. 10 | // "title": "", // Override Title (or use meta comment //title ...) 11 | // "header": "", // Add to 12 | // "body": "", // Add to 13 | // "footer": "", // Add before 14 | } 15 | } 16 | """ 17 | 18 | class OutlineToHtml(sublime_plugin.TextCommand): 19 | # Default settings. 20 | HTML_TITLE = "Hello World" # Replace using //title New Title 21 | HTML_CSS = """ 22 | body { font-size: 1em; font-family: "sans"; background: #222; color: #aaa; padding: 2rem 2.4rem; } 23 | ul { list-style-type: circle; } 24 | a { color: white; } 25 | """ 26 | HTML_HEADER_EXTRA = '' 27 | HTML_BODY_EXTRA = '' 28 | HTML_FOOTER_EXTRA = '' 29 | INDENT_TABS = '\t' # One tab is typical. 30 | INDENT_SPACES = ' ' # If you use something other than 4 spaces. 31 | WS = INDENT_TABS # Whitespace. 32 | HTML_WS = f"\n{WS*2}" 33 | 34 | def run(self, edit): 35 | s = OutlineToHtml 36 | # Can be set by User Preferences. 37 | _s = sublime.load_settings("Preferences.sublime-settings") 38 | s.HTML_TITLE = _s.get("outline_to_html", {}).get("title", s.HTML_TITLE) 39 | s.HTML_CSS = _s.get("outline_to_html", {}).get("css", s.HTML_CSS) 40 | s.HTML_HEADER_EXTRA = _s.get("outline_to_html", {}).get("header", s.HTML_HEADER_EXTRA) 41 | s.HTML_BODY_EXTRA = _s.get("outline_to_html", {}).get("body", s.HTML_BODY_EXTRA) 42 | s.HTML_FOOTER_EXTRA = _s.get("outline_to_html", {}).get("footer", s.HTML_FOOTER_EXTRA) 43 | 44 | parser = OutlineToHtmlCreator() 45 | 46 | # Get current selection 47 | sels = self.view.sel() 48 | sels_parsed = 0 49 | if(len(sels) > 0): 50 | for sel in sels: 51 | # Make sure selection isn't just a cursor 52 | if(abs(sel.b - sel.a) > 0): 53 | self.fromRegion(parser, sel, edit) 54 | sels_parsed += 1 55 | 56 | # All selections just cursor marks? 57 | if(sels_parsed == 0): 58 | region = sublime.Region(0, self.view.size() - 1) 59 | self.fromRegion(parser, region, edit) 60 | 61 | def fromRegion(self, parser, region, edit): 62 | lines = self.view.line(region) 63 | text = self.view.substr(lines) 64 | indented = parser.fromSelection(text) 65 | newview = self.view.window().new_file() 66 | newview.insert(edit, 0, indented) 67 | 68 | class OutlineToHtmlCreator: 69 | def fromSelection(self, text): 70 | return self.create(text.split("\n")) 71 | 72 | def tagify(self, text, token, token_end, formatter, remove_token=False): 73 | pos = 0 74 | found = True 75 | while found: 76 | found = False 77 | ''' Attempt to convert tokens into tags. ''' 78 | if (pos := text.find(token, pos)) >= 0: 79 | found = True 80 | url = '' 81 | url_name = '' 82 | url_end = 0 83 | token_remove = len(token) 84 | # Bail if this token is inside a quote (ex: HTML tag attribute) 85 | if text[pos-1] == '"' or text[pos-1] == "'": 86 | pos = pos+1 87 | continue 88 | # Bail if this ")[" token does not have supporting "[" or ")" 89 | if token == "](": 90 | if text.find('[', 0, pos) == -1 or text.find(')', pos) == -1: 91 | return text 92 | # Extract name. 93 | if token == "](": 94 | url_name = text[text.find('[', 0, pos)+1:pos] 95 | # Extract URL 96 | partial = text[pos:] 97 | for i,c in enumerate(partial): 98 | if c.isspace() or c == token_end: # Will end at whitespace or token_end 99 | url_end = pos+i 100 | break 101 | if not url_end: # We reached EOL 102 | url_end = len(text) 103 | url = text[pos+token_remove:url_end] 104 | if token == "](": # Special case. 105 | text = text[:text.find('[', 0, pos)] + formatter.format(url, url_name) + text[url_end+len(token_end):] 106 | else: 107 | text = text[:pos] + formatter.format(url, url_name) + text[url_end:] 108 | return text 109 | 110 | def create(self, text_iterable): 111 | s = OutlineToHtml 112 | indent_level_previous = 0 113 | inside_code_block = False 114 | output = "" 115 | 116 | # Compile whitespace regex for reuse. 117 | regex_indent = re.compile(f"^({s.INDENT_TABS}|{s.INDENT_SPACES})") 118 | 119 | # Parse text 120 | for line in text_iterable: 121 | 122 | # ```lang Code block. 123 | if(inside_code_block or line.find("```") == 0): 124 | if not inside_code_block and line.find("```") == 0: 125 | if line == "```\n": 126 | language = 'html' 127 | else: 128 | line = line.replace("```", "") 129 | language = line.split("\n")[0] 130 | output += f"
"
131 | 					inside_code_block = True
132 | 					continue
133 | 				# End of code block.
134 | 				elif inside_code_block and line.find("```") == 0:
135 | 					line = line.replace("```", "")
136 | 					output += f"{line}
" 137 | inside_code_block = False 138 | continue 139 | else: 140 | # Inside code block. 141 | output += f"{line}\n" 142 | continue 143 | 144 | # // Metadata comments. 145 | if(line.find("//title ") == 0): 146 | line = line.replace("//title ", "") 147 | s.HTML_TITLE = line 148 | continue 149 | 150 | # // Comments. 151 | if(line.find("//") == 0): 152 | continue # Skip line. 153 | 154 | # Levels of indentation 155 | indent_level = 0 156 | 157 | while(regex_indent.match(line)): 158 | line = regex_indent.sub("", line) 159 | indent_level += 1 160 | 161 | indentDiff = indent_level - indent_level_previous 162 | 163 | # Does a new level of indentation need to be created? 164 | if(indentDiff >= 1): 165 | output += f"{s.HTML_WS}{s.WS*(indent_level-1)}" 184 | ''' 185 | 186 | # Special format line. 187 | prefix = "" 188 | suffix = "" 189 | line = line.strip() 190 | 191 | special = { 192 | "# ": "h1", 193 | "## ": "h2", 194 | "### ": "h3", 195 | "#### ": "h4", 196 | "##### ": "h5", 197 | "###### ": "h6", 198 | "** ": "strong", 199 | "* ": "em", 200 | } 201 | 202 | for key,value in special.items(): 203 | if line.startswith(key): 204 | line = line.replace(key, "") 205 | prefix = f"<{value}>" 206 | suffix = f"" 207 | 208 | line = self.tagify(line, "](", ')', '{1}', remove_token=True) # Named link. 209 | line = self.tagify(line, "https://", ')', '{0}') 210 | line = self.tagify(line, "http://", ')', '{0}') 211 | 212 | 213 | if indent_level > 0: 214 | output += f"{s.HTML_WS}{s.WS*(indent_level)}" + "
  • " + prefix + line + suffix + "
  • " 215 | elif prefix in ['', '']: 216 | output += f"{s.HTML_WS}{s.WS*(indent_level)}" + prefix + line + suffix + "
    " 217 | elif prefix: 218 | output += f"{s.HTML_WS}{s.WS*(indent_level)}" + prefix + line + suffix 219 | else: 220 | output += f"{s.HTML_WS}{s.WS*(indent_level)}" + prefix + line + suffix + "
    " # Plain text. 221 | 222 | indent_level_previous = indent_level 223 | 224 | HTML_HEADER = f""" 225 | 226 | 227 | {s.HTML_TITLE} 228 | 229 | 230 | {s.HTML_HEADER_EXTRA} 231 | 232 | 233 | 234 | 235 | {s.HTML_BODY_EXTRA} 236 | """ 237 | 238 | HTML_FOOTER = f"""{s.HTML_FOOTER_EXTRA} 239 | 240 | 241 | """ 242 | return f"{HTML_HEADER}{output}{HTML_FOOTER}" 243 | 244 | """ 245 | # TODO: Unused currently. For generating from full directories. 246 | def fromFile(self, filename): 247 | input = open(filename, "rU") 248 | output = self.create(input) 249 | input.close() 250 | return output 251 | """ 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✒️ Sublime Outline Notes Publisher 2 | Publish HTML pages using tab indented notes or markdown (md)! It's Obsidian in Sublime Text. 🔥 3 | 4 | Zero dependency publishing from the comfort of your code editor. 5 | 6 | Perfect for: 7 | 8 | * Note takers who love using tab indentation / whitespace for organization. 9 | * Static site generator for personal sites, blogs, micro wiki. 10 | * Zettelkasten 11 | * Replacing your outliner with Sublime Text. 12 | * What is an [Outliner](https://en.wikipedia.org/wiki/Outliner)? 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ## Publish HTML Pages ... 22 | 23 | ![Screenshot](https://user-images.githubusercontent.com/24665/169327275-2b53060d-22ce-40b5-90d1-10c5399d81c2.png) 24 | 25 | ## Using your hierarchy aware notes! 26 | 27 | ```` 28 | All you have to do... 29 | Is create an indented hierarchy. 30 | Of your notes. 31 | In a list. 32 | It will create a nice HTML file... 33 | With a hierarchy based on indent levels. 34 | Isn't this convenient? 35 | Whitespace significant outliner style notes! 36 | Other cool outliners: 37 | [Bike](https://news.ycombinator.com/item?id=31409077) 38 | Dynalist 39 | [Obsidian](https://obsidian.md) 40 | [Obsidian Publish](https://obsidian.md/publish) 41 | Workflowy 42 | Roam Research 43 | Notion 44 | Standard Notes 45 | Evernote 46 | Ever-who? 🐘 47 | Only for: 48 | Sublime Text (https://sublimetext.com) 49 | Super Nintendo 50 | 🔥 Great plugin for 51 | Note taking. 52 | Outlining. 53 | Zero dependency publishing. 54 | 55 | 🚧 Code blocks! 56 | 57 | ```javascript 58 | document.addEventListener("click", ev => { 59 | alert("You selected the following element: " + ev.target) 60 | }) 61 | ``` 62 | 63 | ✅ Common markdown / markup syntax. 64 | 65 | # Header 1 line 66 | ## Header 2 line 67 | * Emphasis line 68 | ** Bold line 69 | 70 | ### Images with img 71 | 72 | 73 | 74 | ### Global and Local Links. Plain, named and pure HTML links. 75 | 76 | http://github.com/gnat/sublime-outliner-html 77 | 🔗 [Local named link!](/local_link) 🌐 [Global named Link!](http://google.com) https://google.com Pure HTML link! 78 | 79 | 💬 Comments. 80 | 81 | // I will not be in the HTML file. 82 | 83 | 🏗️ Comments to insert structural metadata. 84 | 85 | //title I will be added to ▶️ ▶️ 86 | ```` 87 | 88 | ## Installation 89 | 90 | Option A: `Preferences` ➡️ `Package Control` ➡️ `Install Package` ➡️ `Outline to HTML` ➡️ ENTER 91 | 92 | Option B (Direct): `Preferences` ➡️ `Browse Packages ...` ➡️ [Download and extract the latest.](https://github.com/gnat/sublime-outline-notes-publisher/archive/refs/heads/main.zip) 93 | 94 | 95 | ## How to use 96 | 97 | 1. Select text you want to convert. 98 | 2. `CTRL+SHIFT+P` ➡️ `Outline to HTML` 99 | 3. The resulting HTML will open in a new tab. 100 | 101 | Supported languages for [code blocks](https://prismjs.com/#supported-languages) powered by [Prism.js](https://prismjs.com). 102 | 103 | ## Global Configuration (Optional) 104 | 105 | Configure using Preferences ➡ Settings 106 | 107 | ```js 108 | { 109 | // ... 110 | "outline_to_html": { 111 | // "css": "", // Override CSS. 112 | // "title": "", // Override Title (or use meta comment //title ...) 113 | // "header": "", // Add to <head> 114 | // "body": "", // Add to <body> 115 | // "footer": "", // Add before </body> 116 | } 117 | } 118 | ``` 119 | 120 | ## Roadmap 121 | 122 | * Automatic table of contents. 123 | * More metadata comments. 124 | 125 | ## Suggested Sublime Color Schemes 126 | 127 | * [Invader Zim](https://github.com/gnat/sublime-invader-zim) 🛸 128 | 129 | ## Other Cool Outliners / Bullet Point Note Software 130 | 131 | * [Bike](https://www.hogbaysoftware.com/bike/) 132 | * [Dynalist](https://dynalist.io/) 133 | * [Obsidian Publish](https://obsidian.md/publish) 134 | * [Workflowy](https://workflowy.com/) 135 | * [Roam Research](https://roamresearch.com/) 136 | * [Notion](https://www.notion.so/) 137 | * [Standard Notes](https://standardnotes.com/) 138 | * [Evernote](https://www.evernote.com/) 139 | 140 | ## Troubleshooting 141 | 142 | * `View` ➡️ `Show Console` 143 | * Manually invoke for development: `view.run_command('outline_to_html')` 144 | 145 | ## Special Thanks 146 | 147 | * Harrison of [Indent.txt](https://github.com/Harrison-M/indent.txt) for the inspiration for such a plugin. 148 | --------------------------------------------------------------------------------