├── .gitignore ├── README.md ├── __init__.py ├── chrome_to_markdown ├── __init__.py ├── chr_backup.py ├── chr_path.py ├── chrome_to_markdown.py ├── main.py ├── markdown_formatter.py ├── test_chr_backup.py └── test_file_utils.py ├── combine_files └── combine_files.py ├── delete_leading_text └── delete_leading_text.py ├── find_replace ├── create_file.py ├── find_replaceJSON_FF_01.py ├── find_replaceJSON_FF_02.py ├── find_replace_chrome_html.py ├── find_replace_evernote_html.py ├── get_args.py └── replacer.py ├── main.py ├── make_link ├── makelink_evernote_md.py └── makelink_url.py ├── onetab_to_markdown ├── onetab_to_markdown.py └── test.py ├── org_tagged_md_links ├── org_tagged_md_links.py └── test.py ├── setup └── setup.py ├── utils ├── __init__.py ├── arg_utils.py ├── date_append.py ├── file_utils.py ├── get_config.py └── pyperclip_interface.py └── visit_and_tag_md_links ├── __init__.py ├── main.py ├── main_pyperclip.py ├── test.py └── visit_and_tag_md_links.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .vscode/ 3 | 4 | # directories / files 5 | ignored 6 | 7 | 8 | # config (created with script) 9 | 10 | config.json 11 | */config.json 12 | 13 | 14 | # Byte-compiled / optimized / DLL files 15 | .pytest_cache/ 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | env/ 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *,cover 61 | .hypothesis/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | target/ 83 | 84 | # Jupyter Notebook 85 | .ipynb_checkpoints 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # celery beat schedule file 91 | celerybeat-schedule 92 | 93 | # dotenv 94 | .env 95 | 96 | # venv 97 | Include/ 98 | Lib/ 99 | Scripts/ 100 | pyvenv.cfg 101 | 102 | # virtualenv 103 | .venv/ 104 | venv/ 105 | ENV/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # ========================= 114 | # Operating System Files 115 | # ========================= 116 | 117 | # Windows 118 | # ========================= 119 | 120 | desktop.ini 121 | Thumbs.db 122 | ehthumbs.db 123 | *.cab 124 | *.msi 125 | *.msm 126 | *.msp 127 | *.lnk 128 | 129 | # OSX 130 | # ========================= 131 | 132 | .DS_Store 133 | .AppleDouble 134 | .LSOverride 135 | 136 | # Thumbnails 137 | ._* 138 | 139 | # Files that might appear in the root of a volume 140 | .DocumentRevisions-V100 141 | .fseventsd 142 | .Spotlight-V100 143 | .TemporaryItems 144 | .Trashes 145 | .VolumeIcon.icns 146 | 147 | # Directories potentially created on remote AFP share 148 | .AppleDB 149 | .AppleDesktop 150 | Network Trash Folder 151 | Temporary Items 152 | .apdisk 153 | 154 | # ========================= 155 | # Project-specific ignores 156 | # ========================= 157 | 158 | *.bak 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bookmarks to Markdown Utilities 2 | 3 | Collection of command-line tools for Chrome, OneTab, Evernote, and FireFox bookmark management. The tools are mostly to convert proprietary formats to markdown and organizing and add tags. Designed for personal use and other users who may: 4 | 5 | - want to convert bookmarks from various formats into individual markdown files 6 | - collect a lot of bookmarks, want to tag them, but dislike doing it in the browser 7 | - use Chrome for both Desktop and Mobile bookmarking (and mainly use the "Mobile Bookmarks" folder) 8 | - want a way to visit a site and tag the bookmark with minimal mouse movement 9 | - ...and more. 10 | 11 | Note: the configuration file needs to be created with the instructions below: 12 | 13 | - start in root directory 14 | - `python main.py setup` 15 | - This will raise an error if the file already exists. 16 | - `open ./config.json` to open file for editing. 17 | - (Optional) Replace given directories with your own. (see below) 18 | 19 | ```txt 20 | { 21 | "directories": { 22 | "bookmarksRootDir": root directory, 23 | "chromeJSON": directory inside root directory or "", 24 | "chromeMD": directory inside root directory or "", 25 | "combinedFiles": directory inside root directory or "", 26 | "firefoxJson": *location of firefox json file, 27 | "mobileLinksDir": directory inside root directory or "" 28 | }, 29 | 30 | "filenames": { 31 | "chr_md_file_prefix": ex: "chrome.md", 32 | }, 33 | 34 | "markdownFormat": **one of "short", "standard", "long" 35 | } 36 | 37 | ``` 38 | 39 | \* Firefox JSON file location (Mac): "~/Library/Application Support/Google/Chrome/Default/Bookmarks" 40 | 41 | \*\* Formats: 42 | 43 | ```txt 44 | long: 45 | - [name](url) | [url](url) 46 | - [Google Search](http://google.com) | [http://google.com](http://google.com) 47 | 48 | short: 49 | - name 50 | - Google Search 51 | 52 | standard: 53 | - [name](url) 54 | - [Google Search](http://google.com) 55 | ``` 56 | 57 | ## Using the scripts 58 | 59 | This is entirely written in Python 3+. Non-standard modules include: 60 | 61 | - pyperclip 62 | - pytest (for running tests) 63 | 64 | Below are descriptions of each folder with relevant scripts and instructions for running the scripts. The instructions assume you are in the project root in your command line app of choice (terminal.app, iterm.app, etc.). 65 | 66 | ### chrome_to_markdown 67 | 68 | - Backup Chrome 'Bookmarks' file to directory in config file. 69 | - Convert file to markdown format. 70 | - Copy mobile bookmarks to separate file. (this is a call to `delete_leading_text` - see below). 71 | - You may delete the bookmarks in your Chrome mobile bookmarks folder afterwards. 72 | - **Credit for part of this script goes to** [DavidMetcalfe/Chrome-Bookmarks-Parser: Back up and parse Google Chrome's Bookmarks.bak file](https://github.com/DavidMetcalfe/Chrome-Bookmarks-Parser). 73 | 74 | Usage: 75 | 76 | ```bash 77 | # from project root: 78 | python main.py chrome_to_markdown 79 | ``` 80 | 81 | ### combine_files 82 | 83 | Given files of a specified type, their text is combined into one file. 84 | 85 | Usage: 86 | 87 | ```bash 88 | # from project root: 89 | python main.py combine_files [-h] # (help) see necessary arguments 90 | ``` 91 | 92 | ### delete_leading_text 93 | 94 | - Copy mobile bookmarks to separate file. It deletes all links before `# MOBILE BOOKMARKS` 95 | - (Note: this script is called inside `main.py` of "chrome_to_markdown") 96 | 97 | Usage: 98 | 99 | ```bash 100 | # from project root: 101 | python main.py delete_leading_text [output file name] 102 | ``` 103 | 104 | ### find_replace 105 | 106 | - Given a bookmarks html file (commonly used bookmarks backup format), after all styling and classes are removed, we're left with a "clean" html file. (Cleaning is not done inside this codebase.) 107 | - There are multiple scripts that can be run depending on the format: 108 | 109 | 1. `find_replace_chrome_html.py` 110 | 2. `find_replace_evernote_html.py` 111 | 3. `find_replaceJSON_FF_01.py` (Firefox) 112 | 4. `find_replaceJSON_FF_02.py` (Firefox) 113 | 114 | - The html will be converted to markdown. 115 | 116 | Usage: 117 | 118 | ```bash 119 | # from project root: 120 | python main.py find_replace_ [*input file] [*output path] 121 | ``` 122 | 123 | \* Note: only scripts 1 and 2 allow user to specify input and output paths. More documentation is inside the scripts. (<- to fix.) 124 | 125 | ### make_link 126 | 127 | - Given links in a few formats (plain url, Evernote), the text will be converted to markdown. 128 | 129 | 1. `makelink_evernote_md.py` 130 | 2. `makelink_url.py` 131 | 132 | - Follow program instructions for copying text to clipboard. 133 | 134 | Usage: 135 | 136 | ```bash 137 | # from project root: 138 | python main.py makelink_ [-h] # (help) more info 139 | ``` 140 | 141 | ### onetab_to_markdown 142 | 143 | - Given links in OneTab format (the "Import/Export" option), the text will be converted to markdown. 144 | - Follow program instructions for copying text to clipboard. 145 | 146 | Usage: 147 | 148 | ```bash 149 | # from project root: 150 | python main.py onetab_to_markdown [-h] # (help) more info 151 | ``` 152 | 153 | ### org_tagged_md_links 154 | 155 | - Given links in markdown format with "tags", the grouped text will be sorted alphabetically. 156 | - Follow program instructions for copying text to clipboard. 157 | 158 | Usage: 159 | 160 | ```bash 161 | # from project root: 162 | python main.py org_tagged_md_links [-h] # (help) more info 163 | ``` 164 | 165 | ### visit_and_tag_md_links 166 | 167 | Given links in markdown format (all links are in a markdown list, prefixed with '\*'): 168 | 169 | - each link is visited in your default browser, 170 | - then you can "tag" the link in the terminal. (switching can be done with keyboard using CMD+TAB) 171 | - output format from my markdown encoder 172 | 173 | Usage: 174 | 175 | ```bash 176 | # from project root: 177 | python main.py visit_and_tag_md_links 178 | ``` 179 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scraggo/bookmarks-markdown-utils/2fa8a9e66f948b5d486cb9927106b36ecaa68ac4/__init__.py -------------------------------------------------------------------------------- /chrome_to_markdown/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scraggo/bookmarks-markdown-utils/2fa8a9e66f948b5d486cb9927106b36ecaa68ac4/chrome_to_markdown/__init__.py -------------------------------------------------------------------------------- /chrome_to_markdown/chr_backup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | chr_backup.py 5 | Purpose: backs up the Chrome JSON 'Backup' file and adds date to it. 6 | This file can be run independently. 7 | ''' 8 | 9 | # Standard library: 10 | import os 11 | import sys 12 | import shutil 13 | 14 | # Local modules: 15 | import chr_path 16 | 17 | # this import only works if you're in this directory 18 | sys.path.insert(0, '../utils') 19 | import date_append as DA 20 | from file_utils import FileUtils 21 | 22 | 23 | def chrBackup(): 24 | ''' 25 | copy and rename 'Bookmarks' file with date appended to it to 26 | specified directory in config, 27 | ''' 28 | fileUtils = FileUtils() 29 | # Get chrome path of Bookmarks file 30 | chr_fullPath = chr_path.getChromeJSON(chr_path.getChromePath()) 31 | 32 | chr_file = os.path.split(chr_fullPath)[1] 33 | 34 | # Append date 35 | fileWithDate = DA.date_append_filename(chr_file) 36 | 37 | # Return path of copied file - error thrown if exists 38 | return fileUtils.copy_to_chrJsonBackupsDir(chr_fullPath, fileWithDate) 39 | -------------------------------------------------------------------------------- /chrome_to_markdown/chr_path.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | 5 | import os 6 | import platform 7 | import re 8 | import sys 9 | from shutil import copy 10 | # import subprocess 11 | 12 | # Variable declarations 13 | userhome = os.path.expanduser('~').replace('\\', '\\\\') 14 | useros = platform.system() 15 | destination = os.path.dirname(os.path.realpath(__file__)) 16 | # cmd = ['python', 'Chrome_Bookmarks_MinHTML.py'] 17 | 18 | 19 | def getChromePath(): 20 | 21 | # Directory references for Chrome pulled from Chromium docs. 22 | # https://www.chromium.org/user-experience/user-data-directory 23 | 24 | vernum = platform.version() 25 | 26 | # Check if platform is Windows. 27 | if useros is 'Windows': 28 | # Determine if OS is XP. 29 | if re.search('^5\.', vernum): 30 | profilePath = os.path.normpath(os.path.join( 31 | userhome, 'Local Settings\\Application Data\\' 32 | 'Google\Chrome\\User Data\\Default')) 33 | filePath = os.path.normpath(os.path.join( 34 | profilePath, "Bookmarks.bak")) 35 | if os.path.exists(filePath): 36 | copy(filePath, destination) 37 | else: 38 | sys.exit("Cannot find Bookmarks.bak file.") 39 | 40 | # Else, assume OS is 10 / 8 / 7 / Vista. 41 | else: 42 | profilePath = os.path.normpath(os.path.join( 43 | userhome, 'AppData\\Local\\Google\\Chrome' 44 | '\\User Data\\Default')) 45 | filePath = os.path.normpath(os.path.join( 46 | profilePath, "Bookmarks.bak")) 47 | if os.path.exists(filePath): 48 | copy(filePath, destination) 49 | else: 50 | sys.exit("Cannot find Bookmarks.bak file.") 51 | 52 | # Check if platform is Mac OS X. 53 | elif useros == 'Darwin': 54 | DARWIN_PATH = 'Library/Application Support/Google/Chrome/Default' 55 | profilePath = os.path.normpath(os.path.join( 56 | userhome, DARWIN_PATH)) 57 | if os.path.exists(profilePath): 58 | return profilePath 59 | else: 60 | sys.exit("Cannot find Chrome Library path.") 61 | 62 | # Check if platform is Linux. 63 | elif useros == 'Linux': 64 | profilePath = os.path.normpath(os.path.join( 65 | userhome, '/.config/google-chrome/Default')) 66 | filePath = os.path.normpath(os.path.join( 67 | profilePath, "Bookmarks.bak")) 68 | if os.path.exists(filePath): 69 | copy(filePath, destination) 70 | else: 71 | sys.exit("Cannot find Bookmarks.bak file.") 72 | 73 | # print(getChromePath()) #tested, works 74 | 75 | 76 | def getChromeBak(chrPath=None): 77 | if not chrPath: 78 | chrPath = getChromePath() 79 | filePath = os.path.join( 80 | chrPath, "Bookmarks.bak") 81 | # print(filePath) #debug 82 | if os.path.exists(filePath): 83 | # copy(filePath, destination) 84 | return filePath 85 | else: 86 | sys.exit("Cannot find Bookmarks.bak file.") 87 | 88 | 89 | def getChromeJSON(chrPath=None): 90 | '''This is the main bookmarks file, without .bak extension.''' 91 | if not chrPath: 92 | chrPath = getChromePath() 93 | JSONfilePath = os.path.join( 94 | chrPath, "Bookmarks") 95 | if os.path.exists(JSONfilePath): 96 | return JSONfilePath 97 | else: 98 | sys.exit("Cannot find main Chrome Bookmarks file.") 99 | -------------------------------------------------------------------------------- /chrome_to_markdown/chrome_to_markdown.py: -------------------------------------------------------------------------------- 1 | # chrome_to_markdown.py 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | """ 5 | Created on 4/26/17, 11:18 AM 6 | 7 | @author: davecohen 8 | 9 | Title: Convert Chrome Bookmarks JSON file to Markdown 10 | 11 | Output Format: 12 | * [site name](http://example.com) | [http://example.com](http://example.com) 13 | 14 | Input Example: (json) 15 | "name": "Site Name", 16 | "url": "chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#uri=http://example.com" 17 | 18 | parsing: (standard format) 19 | delete: "type": "url", 20 | replace: 21 | "name": with [ ] 22 | "url": with ( ) 23 | """ 24 | 25 | # Standard library: 26 | import os 27 | import sys 28 | import re 29 | import shutil 30 | 31 | # Local modules: 32 | from chr_path import getChromeJSON 33 | from markdown_formatter import markdownFormatMap 34 | 35 | # this import only works if you're in this directory 36 | sys.path.insert(0, '../utils') 37 | import date_append as DA 38 | from file_utils import FileUtils 39 | from get_config import get_json_config 40 | 41 | config = get_json_config() 42 | 43 | 44 | fileUtils = FileUtils() 45 | 46 | 47 | class MarkdownCreator: 48 | def __init__(self): 49 | self.md_output = '' 50 | 51 | def deleter(self, f_str): 52 | '''deletes type, url in f_str 53 | ''' 54 | return f_str\ 55 | .replace('"type": "url",', '') 56 | 57 | def replacer(self, f_str): 58 | '''replaces html tags 59 | ''' 60 | return f_str \ 61 | .replace('\n', '\n - ') 63 | 64 | def get_confirmation(self): 65 | # CHECKS 66 | print(''' 67 | DON'T FORGET: 68 | -make sure that your mobile bookmarks have synced to desktop 69 | -if you feel sure that you're ready, you can delete bookmarks after this backup. 70 | READY? hit Enter. 71 | Not ready? Type any key then Enter to exit.''') 72 | yes = input('') 73 | if yes != '': 74 | return False 75 | return True 76 | 77 | def create_output_path(self): 78 | md_oTemp = os.path.join(config['directories']['bookmarksRootDir'], config['directories'] 79 | ['chromeMD'], config['filenames']['chr_md_file_prefix']) 80 | self.md_output = DA.date_append(md_oTemp) 81 | 82 | def write_bookmarks_to_file(self, _chrJSON): 83 | output = open(self.md_output, 'w') 84 | 85 | MD_FORMAT = config['markdownFormat'] 86 | MD_FUNCS = markdownFormatMap[MD_FORMAT] 87 | handle_name = MD_FUNCS['name'] 88 | handle_url = MD_FUNCS['url'] 89 | 90 | with open(_chrJSON) as f: 91 | for line in f: 92 | line = line.strip() 93 | line = self.deleter(line) 94 | if line.startswith('"name"'): 95 | output.write(handle_name(line)) 96 | elif line.startswith('"url"'): 97 | line = re.sub(r"(chrome-extension://)(.+)(uri=)", "", line) 98 | output.write(handle_url(line) + '\n') 99 | elif line == '"synced": {': 100 | output.write('\n\n# MOBILE BOOKMARKS\n\n') 101 | 102 | output.close() 103 | 104 | print('Successfully converted json bookmarks backup to markdown!') 105 | print('Output to:', self.md_output) 106 | 107 | def run_script(self): 108 | if not self.get_confirmation(): 109 | return sys.exit('Exiting.') 110 | 111 | self.create_output_path() 112 | 113 | # Markdown file - check output location 114 | fileUtils.fileExists(self.md_output) # raises error if exists 115 | 116 | # Create Markdown from original "Bookmarks" file 117 | chrJSON = getChromeJSON() 118 | 119 | # print('chrJSON', chrJSON)#debug 120 | 121 | self.write_bookmarks_to_file(chrJSON) 122 | 123 | # print('done') #debug 124 | -------------------------------------------------------------------------------- /chrome_to_markdown/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Title: Convert Chrome Bookmarks JSON file to Markdown 6 | and backup Chrome Bookmarks 7 | """ 8 | 9 | import os 10 | import sys 11 | import subprocess 12 | 13 | from chr_backup import chrBackup 14 | from chrome_to_markdown import MarkdownCreator 15 | # from move_files import move_files 16 | 17 | # this import only works if you're in this directory 18 | sys.path.insert(0, '../utils') 19 | from get_config import get_json_config 20 | 21 | config = get_json_config() 22 | 23 | 24 | def test_things(): 25 | md_creator = MarkdownCreator() 26 | # md_creator.run_script() 27 | md_creator.create_output_path() 28 | new_file_path = md_creator.md_output 29 | path, file_name = os.path.split(new_file_path) 30 | print(path, file_name) 31 | 32 | 33 | def main(): 34 | chrBackup() # Backup bookmarks 35 | md_creator = MarkdownCreator() 36 | md_creator.run_script() 37 | new_file_path = md_creator.md_output 38 | # path, file_name = os.path.split(new_file_path) 39 | # print('Split up the path and file name:\n', path, file_name) 40 | # move_files(new_file_path) 41 | subprocess.run( 42 | ['python', '../delete_leading_text/delete_leading_text.py', new_file_path]) 43 | 44 | 45 | if __name__ == '__main__': 46 | # test_things() 47 | main() 48 | -------------------------------------------------------------------------------- /chrome_to_markdown/markdown_formatter.py: -------------------------------------------------------------------------------- 1 | """Long Format 2 | - [name](url) | [url](url) 3 | 4 | example: 5 | - [Google Search](http://google.com) | [http://google.com](http://google.com) 6 | """ 7 | 8 | 9 | def handle_long_name(line): 10 | return line\ 11 | .replace('"name": "', '* [')\ 12 | .replace('",', ']') 13 | 14 | 15 | def handle_long_url(line): 16 | line = line.replace('"url": "', '(') 17 | line_paren = line.replace('"', ')') 18 | return line_paren + ' | ' + \ 19 | line_paren.replace( 20 | '(', '[').replace(')', ']') + line_paren 21 | 22 | 23 | """Short Format 24 | - name 25 | 26 | example: 27 | - Google Search 28 | """ 29 | 30 | 31 | def handle_short_name(line): 32 | return line\ 33 | .replace('"name": "', '* ')\ 34 | .replace('",', ' ') 35 | 36 | 37 | def handle_short_url(line): 38 | return line\ 39 | .replace('"url": "', '<')\ 40 | .replace('"', '>') 41 | 42 | 43 | """Standard Format 44 | - [name](url) 45 | 46 | example: 47 | - [Google Search](http://google.com) 48 | """ 49 | 50 | 51 | def handle_standard_name(line): 52 | return handle_long_name(line) 53 | 54 | 55 | def handle_standard_url(line): 56 | return line\ 57 | .replace('"url": "', '(')\ 58 | .replace('"', ')') 59 | 60 | 61 | markdownFormatMap = { 62 | "long": { 63 | "name": handle_long_name, 64 | "url": handle_long_url 65 | }, 66 | "short": { 67 | "name": handle_short_name, 68 | "url": handle_short_url 69 | }, 70 | "standard": { 71 | "name": handle_standard_name, 72 | "url": handle_standard_url 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /chrome_to_markdown/test_chr_backup.py: -------------------------------------------------------------------------------- 1 | 2 | import pytest 3 | import shutil 4 | import os 5 | import sys 6 | 7 | from .chr_backup import chrBackup 8 | 9 | cwd = os.getcwd() 10 | 11 | # this import only works if you're in this directory 12 | sys.path.insert(0, '../utils') 13 | from file_utils import FileUtils 14 | 15 | fUtil = FileUtils() 16 | 17 | 18 | def test_copy_to_chromeJSON(): 19 | path = chrBackup() # creates file in default dir 20 | assert isinstance(path, str) 21 | assert len(path) > 0 22 | -------------------------------------------------------------------------------- /chrome_to_markdown/test_file_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import shutil 3 | import os 4 | import sys 5 | 6 | # this import only works if you're in this directory 7 | sys.path.insert(0, '../utils') 8 | from file_utils import FileUtils 9 | 10 | fUtil = FileUtils() 11 | 12 | cwd = os.getcwd() 13 | 14 | 15 | def create_file(fName='test_file.txt'): 16 | f = open(fName, "w+") 17 | return fName 18 | 19 | 20 | def delete_file_or_dir(path, dir=False): 21 | if dir: 22 | shutil.rmtree(path) 23 | else: 24 | os.remove(path) 25 | 26 | 27 | def test_file_exists(): 28 | fName = create_file() 29 | with pytest.raises(OSError): 30 | fUtil.fileExists(fName) 31 | delete_file_or_dir(fName) 32 | assert fUtil.fileExists(fName) is None 33 | 34 | 35 | def test_join_path(): 36 | topDir = '-testDir' 37 | assert fUtil.join_path(topDir, cwd) == cwd + '/-testDir' 38 | 39 | 40 | def test_copy_file(): 41 | fName1 = create_file() # in original directory 42 | topDir = 'zzz-testDir' # create new directory 43 | inputFilePath = os.path.join(cwd, fName1) 44 | outputDir = os.path.join(cwd, topDir) 45 | outputFilePath = os.path.join(cwd, topDir) 46 | os.mkdir(outputFilePath) 47 | fUtil.copy_file(inputFilePath, outputDir) 48 | assert os.path.exists(outputFilePath) is True 49 | delete_file_or_dir(fName1) 50 | delete_file_or_dir(outputFilePath, dir=True) 51 | -------------------------------------------------------------------------------- /combine_files/combine_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Title: Combine files 5 | 6 | usage: combine_files.py [-h] [-o OUTDIR | -d] [-f FILEOUTNAME] dir type 7 | 8 | Given files of a specified type, their text is combined into one file. 9 | 10 | positional arguments: 11 | dir directory which contains files 12 | type type of file (eg: html, txt, md) 13 | 14 | optional arguments: 15 | -h, --help show this help message and exit 16 | -o OUTDIR, --outdir OUTDIR 17 | output file directory (default ) 18 | -d, --default use output directory specified in config.json 19 | -f FILEOUTNAME, --fileoutname FILEOUTNAME 20 | output file name (default: `--combined.` 21 | """ 22 | import argparse 23 | import os 24 | import sys 25 | 26 | # this import only works if you're in this directory 27 | sys.path.insert(0, '../utils') 28 | from get_config import get_json_config 29 | 30 | 31 | def create_dir(dir): 32 | # create dir if it doesn't exist 33 | if not os.path.exists(dir): 34 | os.makedirs(dir) 35 | 36 | 37 | def combine_files(outfile_path, file_iterator, file_path): 38 | # Combines files with specified filetype 39 | with open(outfile_path, 'w') as outfile: 40 | for fname in file_iterator: 41 | fname = os.path.join(file_path, fname) 42 | with open(fname) as infile: 43 | for line in infile: 44 | outfile.write(line) 45 | 46 | print('Success. see', outfile_path) 47 | 48 | 49 | def get_args(): 50 | parser = argparse.ArgumentParser( 51 | description="Given files of a specified type, their text is combined into one file.") 52 | group = parser.add_mutually_exclusive_group() 53 | group.add_argument( 54 | "-o", "--outdir", help="output file directory (default )") 55 | group.add_argument("-d", "--default", action="store_true", 56 | help="use output directory specified in config.json") 57 | parser.add_argument("-f", "--fileoutname", 58 | help="output file name (default: `--combined.`") 59 | parser.add_argument("dir", help="directory which contains files") 60 | parser.add_argument("type", help="type of file (eg: html, txt, md)") 61 | return parser.parse_args() 62 | 63 | 64 | def create_file_iterator(file_path, file_type, outfile_name): 65 | filenames = os.listdir(file_path) 66 | return (x for x in filenames if x.endswith(file_type) and not x == outfile_name) 67 | 68 | 69 | def main(): 70 | args = get_args() 71 | filepath = args.dir 72 | 73 | if args.type.startswith('.'): 74 | filetype = args.type 75 | else: 76 | filetype = '.' + args.type 77 | 78 | if args.fileoutname: 79 | outfilename = args.fileoutname + filetype 80 | else: 81 | outfilename = '--combined' + filetype 82 | 83 | # handle output directory cases 84 | if args.default: 85 | # import the path from config 86 | config_dir = get_json_config()['directories'] 87 | rootdir = config_dir['bookmarksRootDir'] 88 | combined_dir = config_dir['combinedFiles'] 89 | if combined_dir == '': 90 | outfilepath = os.path.join(rootdir, outfilename) 91 | else: 92 | create_dir(os.path.join(rootdir, combined_dir)) 93 | outfilepath = os.path.join(rootdir, combined_dir, outfilename) 94 | elif args.outdir: 95 | create_dir(args.outdir) 96 | outfilepath = os.path.join(args.outdir, outfilename) 97 | else: 98 | outfilepath = os.path.join(filepath, outfilename) 99 | 100 | # print(filepath, filetype, outfilename, outfilepath, args.default, args.outdir)#debug 101 | 102 | file_generator = create_file_iterator(filepath, filetype, outfilename) 103 | 104 | combine_files(outfilepath, file_generator, filepath) 105 | 106 | 107 | if __name__ == '__main__': 108 | main() 109 | -------------------------------------------------------------------------------- /delete_leading_text/delete_leading_text.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # this import only works if you're in this directory 5 | sys.path.insert(0, '../utils') 6 | from get_config import get_json_config 7 | 8 | dirs = get_json_config()['directories'] 9 | 10 | constants = { 11 | 'title': '## MOBILE BOOKMARKS', 12 | 'outputMessage': 'Successfully made a copy of your mobile bookmarks folder as markdown. Output to: ', 13 | 'extension': '-mobile.md' 14 | } 15 | 16 | description = '''error: expected inputFilename 17 | 18 | Usage: 19 | Command line: python delete_leading_text.py inputFilename [Optional: output path] 20 | 21 | This script is called after chrome-to-markdown. It copies the markdown file to a new directory and removes all the text above `## MOBILE BOOKMARKS`. 22 | ''' 23 | 24 | if len(sys.argv) < 2: 25 | sys.exit(description) 26 | 27 | inputFilePath = sys.argv[1] 28 | 29 | outputFilePath = os.path.join(dirs['bookmarksRootDir'], dirs['mobileLinksDir']) 30 | 31 | # optional output path: 32 | if len(sys.argv) > 2: 33 | outputFilePath = sys.argv[2] 34 | 35 | date = inputFilePath[-9:-3] # get date 36 | outputFilename = date + constants['extension'] 37 | fullOutputPath = os.path.join(outputFilePath, outputFilename) 38 | 39 | if os.path.exists(fullOutputPath): 40 | sys.exit('File exists: ' + fullOutputPath) 41 | 42 | 43 | def writeToFile(): 44 | with open(fullOutputPath, 'w') as outfile: 45 | found = False 46 | with open(inputFilePath, 'r') as infile: 47 | for line in infile: 48 | if constants['title'] in line: 49 | found = True 50 | if found: 51 | outfile.write(line.strip() + '\n') 52 | 53 | 54 | writeToFile() 55 | print(constants['outputMessage'] + fullOutputPath) 56 | -------------------------------------------------------------------------------- /find_replace/create_file.py: -------------------------------------------------------------------------------- 1 | def create_file(outputlocation, write_mode, textfile, replace_func): 2 | with open(outputlocation, write_mode) as output: 3 | with open(textfile, 'r') as f: 4 | for line in f: 5 | line = line.strip() 6 | line = replace_func(line) 7 | output.write(line) 8 | print('Output to:', outputlocation) 9 | -------------------------------------------------------------------------------- /find_replace/find_replaceJSON_FF_01.py: -------------------------------------------------------------------------------- 1 | #pythontemplate 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | """ 5 | Created on 4/26/17, 11:18 AM 6 | 7 | @author: davecohen 8 | 9 | Title: Convert Firefox Backup (JSON) to Markdown Links 10 | 11 | """ 12 | import os, sys, re, json 13 | 14 | from replacer import replacer 15 | from create_file import create_file 16 | 17 | # this import only works if you're in this directory 18 | sys.path.insert(0, '../utils') 19 | from get_config import get_json_config 20 | 21 | def get_replaced_line(line): 22 | '''This function is file-specific 23 | ''' 24 | to_delete = [ 25 | ['"type": "url",', ''] 26 | ] 27 | 28 | to_replace_name = [ 29 | ['"name": "', '* ['], 30 | ['",', ']'] 31 | ] 32 | 33 | to_replace_paren = [ 34 | ['(', '['], 35 | [')', ']'] 36 | ] 37 | 38 | line = replacer(to_delete, line) 39 | if line.startswith('"name"'): 40 | return replacer(to_replace_name, line) 41 | elif line.startswith('"url"'): 42 | if 'chrome-extension://' in line: 43 | line = re.sub(r"chrome-extension://([a-z])+/suspended.html#uri=", "", line) 44 | line = line.replace('"url": "', '(') 45 | line_paren = line.replace('"', ')') 46 | line = line_paren + ' | ' + replacer(to_replace_paren, line_paren) + line_paren 47 | return line + '\n' 48 | return '' 49 | 50 | def main(): 51 | # get paths 52 | config = get_json_config() 53 | # load firefox json file 54 | with open(config['directories']['firefoxJson'], encoding='utf-8') as data_file: 55 | data = json.loads(data_file.read()) 56 | 57 | ff_json_list = str(data).split(', ') 58 | 59 | output_filename = 'firefox_output.html' 60 | outputlocation = os.path.join(config['directories']['bookmarksRootDir'], output_filename) 61 | 62 | 63 | if os.path.exists(outputlocation): 64 | print('Save over ' + outputlocation + '?') 65 | quit = input('Press y if ok. If not ok, press enter. ') 66 | if quit.lower() != 'y': 67 | sys.exit('Quitting.') 68 | 69 | create_file(outputlocation, 'w', ff_json_list, get_replaced_line) 70 | 71 | if __name__ == '__main__': 72 | main() 73 | -------------------------------------------------------------------------------- /find_replace/find_replaceJSON_FF_02.py: -------------------------------------------------------------------------------- 1 | #pythontemplate 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | """ 5 | Created on 4/26/17, 11:18 AM 6 | 7 | @author: davecohen 8 | 9 | Title: Convert Firefox Backup (JSON) to Markdown Links 10 | """ 11 | import os, sys, re 12 | import json 13 | from pprint import pprint 14 | 15 | # this import only works if you're in this directory 16 | sys.path.insert(0, '../utils') 17 | from get_config import get_json_config 18 | 19 | def deleter(str): 20 | if str.endswith("'") or\ 21 | str.endswith("]") or\ 22 | str.endswith("}"): 23 | return(deleter(str[:-1])) # Note: this is recursive 24 | else: 25 | return str 26 | 27 | def replacer(str): 28 | str = str \ 29 | .replace('\n', '\n - ') 31 | return str 32 | 33 | def main(): 34 | # get paths 35 | config = get_json_config() 36 | 37 | # load firefox json file 38 | with open(config["firefoxJson"], encoding='utf-8') as data_file: 39 | data = json.loads(data_file.read()) 40 | 41 | ff_json_list = str(data).split(', ') 42 | 43 | output_filename = 'firefox_output.html' 44 | outputlocation = os.path.join(config['directories']['bookmarksRootDir'], output_filename) 45 | 46 | if os.path.exists(outputlocation): 47 | print('Save over ' + outputlocation + '?') 48 | quit = input('Press y if ok. If not ok, press enter. ') 49 | if quit.lower() != 'y': 50 | sys.exit('Quitting.') 51 | 52 | with open(outputlocation, 'w') as output: 53 | for line in ff_json_list: 54 | line = line.strip() 55 | line = deleter(line) 56 | if line[1:6] == 'title' and len(line) > 11: 57 | line = '* [' + line[10:] + ']' #was 10:-1 58 | output.write(line) 59 | elif line[1:4] == 'uri': 60 | line = '(' + line[8:] + ')' #was 8:-2 61 | output.write(line + '\n') 62 | 63 | print('Output to:', outputlocation) 64 | 65 | 66 | if __name__ == '__main__': 67 | main() -------------------------------------------------------------------------------- /find_replace/find_replace_chrome_html.py: -------------------------------------------------------------------------------- 1 | # pythontemplate 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | """ 5 | Created on 4/26/17, 11:18 AM 6 | 7 | @author: davecohen 8 | 9 | usage: 10 | 'python find_replace_clean_html.py []' 11 | 12 | Title: Find / Replace in HTML 13 | """ 14 | import os 15 | import json 16 | import sys 17 | 18 | from replacer import replacer 19 | from create_file import create_file 20 | from get_args import get_args 21 | 22 | # this import only works if you're in this directory 23 | sys.path.insert(0, '../utils') 24 | from get_config import get_json_config 25 | 26 | 27 | def get_replaced_line(line): 28 | '''This function is file-specific 29 | ''' 30 | to_delete = [ 31 | ['

', ''], 32 | ['

', ''], 33 | ['
', ''], 34 | ['
', ''] 35 | ] 36 | 37 | to_replace = [ 38 | ['
', '
    '], 39 | ['
', ''], 40 | ['
', '
  • '], 41 | ['
  • ', ''] 42 | ] 43 | if line == '': 44 | return line 45 | 46 | replaced = replacer(to_delete + to_replace, line) 47 | if replaced == '': 48 | return replaced 49 | 50 | return replaced + '\n' 51 | 52 | 53 | def main(): 54 | description_str = '''This script finds and replaces or removes certain html tags. 55 | It can be ran after 56 | - exporting a bookmarks.html file from Chrome 57 | - "clearning" html (I used https://html-cleaner.com/)''' 58 | 59 | args = get_args( 60 | description_str, 61 | { 62 | "flag": "html_file", 63 | "help": "chrome html file" 64 | }, 65 | { 66 | "flag": "-o", 67 | "help": "full output file path and name" 68 | } 69 | ) 70 | 71 | html_file = args.html_file 72 | output_location = args.o 73 | if not output_location: 74 | output_filename = 'chrome_output.html' 75 | config = get_json_config() 76 | outputlocation = os.path.join( 77 | config['directories']['bookmarksRootDir'], output_filename) 78 | 79 | write_mode = 'w' 80 | create_file(outputlocation, write_mode, html_file, get_replaced_line) 81 | 82 | 83 | if __name__ == '__main__': 84 | main() 85 | -------------------------------------------------------------------------------- /find_replace/find_replace_evernote_html.py: -------------------------------------------------------------------------------- 1 | #pythontemplate 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | """ 5 | Created on 4/26/17, 11:18 AM 6 | 7 | @author: davecohen 8 | 9 | usage: 10 | 'python find_replace_clean_html.py []' 11 | 12 | Title: Find / Replace in HTML 13 | """ 14 | import os, json, sys 15 | 16 | from replacer import replacer 17 | from create_file import create_file 18 | from get_args import get_args 19 | 20 | # this import only works if you're in this directory 21 | sys.path.insert(0, '../utils') 22 | from get_config import get_json_config 23 | 24 | def get_replaced_line(line): 25 | '''This function is file-specific 26 | ''' 27 | to_delete = [ 28 | ['
     
    ', ''], 29 | ['

    ', ''], 30 | ['
    ', ''], 31 | ['
    ', ''] 32 | ] 33 | 34 | to_replace = [ 35 | ['\n', '\n - '] 37 | ] 38 | if line == '': 39 | return line 40 | 41 | line = line.replace('

    ', '

    ') 42 | line = replacer(to_delete, line) 43 | line = line.strip() 44 | 45 | if line == '': 46 | return line 47 | 48 | return replacer(to_replace, line) + '\n' 49 | 50 | def main(): 51 | description_str = '''Description: 52 | This script finds and replaces or removes certain html tags. 53 | It can be ran after 54 | - exporting an html file from Evernote 55 | - "clearning" html (I used https://html-cleaner.com/)''' 56 | 57 | args = get_args( 58 | description_str, 59 | { 60 | "flag": "html_file", 61 | "help": "evernote html file" 62 | }, 63 | { 64 | "flag": "-o", 65 | "help": "full output file path and name" 66 | } 67 | ) 68 | 69 | html_file = args.html_file 70 | output_location = args.o 71 | if not output_location: 72 | output_filename = 'evernote_output.html' 73 | config = get_json_config() 74 | outputlocation = os.path.join(config['directories']['bookmarksRootDir'], output_filename) 75 | 76 | create_file(outputlocation, 'w', html_file, get_replaced_line) 77 | 78 | if __name__ == '__main__': 79 | main() 80 | -------------------------------------------------------------------------------- /find_replace/get_args.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | def get_args(desc_str, infile_obj, outfile_obj): 4 | '''obj: { 5 | flag: '' 6 | help: '' 7 | } 8 | ''' 9 | parser = argparse.ArgumentParser(description=desc_str) 10 | inflag = infile_obj['flag'] 11 | outflag = outfile_obj['flag'] 12 | parser.add_argument(inflag, help=infile_obj['help']) 13 | parser.add_argument(outflag, help=outfile_obj['help']) 14 | return parser.parse_args() 15 | -------------------------------------------------------------------------------- /find_replace/replacer.py: -------------------------------------------------------------------------------- 1 | def replacer(arr, str): 2 | """Given list 'arr' of modifications, 3 | return str with specified modifications. 4 | params: 5 | arr: list of lists [string to find, string to replace with] 6 | str: string 7 | Caution - watch out for unicode chars. Ex: ' ' is unicode \xa0 8 | (this seems to only occur when replacers are tuples) 9 | """ 10 | for item in arr: 11 | str = str.replace(item[0], item[1]) 12 | return str 13 | 14 | def run_tests(): 15 | # quick tests 16 | replacers = [ 17 | ['

    ', ''], 18 | ['

    ', ''], 19 | ['
    ', ''], 20 | ['
    ', ''] 21 | ] 22 | # TEST 1 23 | s1 = '
    \nTesting!' 24 | t1 = replacer( 25 | [replacers[0]], 26 | s1 27 | ) 28 | exp1 = '\nTesting!' 29 | assert exp1 == t1, t1 30 | # TEST 2 31 | s2 = '
    \nTesting!

    P was here.
    Div was here.' 32 | t2 = replacer( 33 | replacers[:], 34 | s2 35 | ) 36 | exp2 = '\nTesting!P was here.Div was here.' 37 | assert exp2 == t2, t2 38 | print('all tests passed!') 39 | 40 | def main(): 41 | run_tests() 42 | 43 | if __name__ == '__main__': 44 | main() 45 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from utils import arg_utils 4 | 5 | rest_of_args = arg_utils.get_rest_of_args(sys.argv[1:]) 6 | arg_utils.execute_args(rest_of_args) 7 | -------------------------------------------------------------------------------- /make_link/makelink_evernote_md.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | help = """ 4 | Title: Evernote Clipped Bookmark to Markdown URL List element 5 | 6 | Description: Copy links to clipboard in the input format. They'll be sent to your clipboard in the output format. 7 | 8 | Usage: Follow program instructions for copying text to clipboard. 9 | 10 | Example input: 11 | site title | site.com 12 | 13 | [http://example.com](http://example.com) - site description here... 14 | 15 | Example output: 16 | * site title | site.com - [http://example.com](http://example.com) - site description here 17 | """ 18 | 19 | import re 20 | import sys 21 | # this import only works if you're in this directory 22 | sys.path.insert(0, '../utils') 23 | from pyperclip_interface import PyperInterface 24 | import arg_utils 25 | 26 | 27 | def reformat_links(linklist): 28 | find_md_regex = re.compile(r'^\[https?.*\]\(https?.*\)') 29 | newformat = [] 30 | for line in linklist: 31 | if line == '': 32 | continue 33 | if find_md_regex.match(line): 34 | newformat.append(line) 35 | else: 36 | newformat.append('\n\n* {} - '.format(line)) 37 | return newformat 38 | 39 | 40 | def main(): 41 | arg_utils.show_help_if_arg(sys.argv, help) 42 | in_message = 'Copy your Evernote links to clipboard, press ENTER when done:' 43 | out_message = 'Finished!\nMarkdown links were copied to your clipboard.' 44 | pyper = PyperInterface(in_message, out_message) 45 | 46 | links = pyper.get_clipboard() 47 | linklist = links.split('\n') 48 | 49 | reformatted = reformat_links(linklist) 50 | pyper.send_to_clipboard(''.join(reformatted)) 51 | 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /make_link/makelink_url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | help = """ 4 | Title: URL to Markdown URL List element 5 | 6 | Description: Copy links to clipboard in the input format. They'll be sent to your clipboard in the output format. 7 | 8 | Example input: 9 | https://example1.com 10 | https://example2.com 11 | 12 | Example output: 13 | * [https://example1.com](https://example1.com) 14 | * [https://example2.com](https://example2.com) 15 | """ 16 | 17 | import re 18 | import sys 19 | # this import only works if you're in this directory 20 | sys.path.insert(0, '../utils') 21 | from pyperclip_interface import PyperInterface 22 | import arg_utils 23 | 24 | 25 | def reformat_links(linklist): 26 | newformat = [] 27 | for line in linklist: 28 | line = line.strip() 29 | if line == '': 30 | continue 31 | else: 32 | newformat.append('* [{}]({})'.format(line, line)) 33 | return newformat 34 | 35 | 36 | def main(): 37 | arg_utils.show_help_if_arg(sys.argv, help) 38 | in_message = 'Copy your URL links to clipboard, press ENTER when done:' 39 | out_message = 'Finished!\nMarkdown links were copied to your clipboard.' 40 | pyper = PyperInterface(in_message, out_message) 41 | 42 | links = pyper.get_clipboard() 43 | linklist = links.split('\n') 44 | 45 | reformatted = reformat_links(linklist) 46 | pyper.send_to_clipboard('\n'.join(reformatted)) 47 | 48 | 49 | if __name__ == '__main__': 50 | main() 51 | -------------------------------------------------------------------------------- /onetab_to_markdown/onetab_to_markdown.py: -------------------------------------------------------------------------------- 1 | # pythontemplate 2 | #!/usr/bin/env python3 3 | # -*- coding: utf-8 -*- 4 | 5 | help = """ 6 | Title: Onetab export to Markdown URL List element 7 | 8 | Description: Copy links to clipboard in the input format. They'll be sent to your clipboard in the output format. 9 | 10 | Example input: 11 | https://example1.com | site title1 12 | https://example2.com | site title2 13 | 14 | Example output: 15 | * [https://example1.com](https://example1.com) 16 | * [https://example2.com](https://example2.com) 17 | """ 18 | 19 | import re 20 | import string 21 | import sys 22 | import pyperclip 23 | sys.path.insert(0, '../utils') 24 | import arg_utils 25 | 26 | class App: 27 | 28 | linkRegex = re.compile(r'(https?\S*)( \| )(.*)') 29 | suspRegex = r"(chrome-extension://)(.+)(uri=)" 30 | # printable = set(string.printable) 31 | 32 | def __init__(self, in_text): 33 | # remove non-ascii - not working 34 | # self.in_text = ''.join(filter(lambda x: ord(x) <= 126, in_text)) 35 | self.in_text = re.sub(self.suspRegex, '', in_text) 36 | self.in_list = in_text.split('\n') 37 | self.out_list = [] 38 | self.make_link() 39 | 40 | def make_link(self): 41 | for line in self.in_list: 42 | found_links = self.re_search(line) 43 | if found_links: 44 | md_link = '- [{}]({})'.format(found_links[1], found_links[0]) 45 | self.out_list.append(md_link) 46 | else: 47 | self.out_list.append(line.replace('http://', '## ')) 48 | 49 | def return_links(self): 50 | return '\n'.join(self.out_list) 51 | 52 | def re_search(self, f_line): 53 | link_mo = self.linkRegex.search(f_line) 54 | if link_mo: 55 | # open link if found 56 | return link_mo.group(1), link_mo.group(3) 57 | return None 58 | 59 | 60 | def main(): 61 | arg_utils.show_help_if_arg(sys.argv, help) 62 | print('Copy your markdown links to clipboard, enter when done:') 63 | input('> ') 64 | linkTxt = pyperclip.paste() 65 | 66 | links = App(linkTxt) 67 | links.make_link() 68 | input('Press Enter to copy sorted links to your clipboard.') 69 | pyperclip.copy(links.return_links()) 70 | print('Sorted links copied!') 71 | 72 | if __name__ == '__main__': 73 | main() 74 | -------------------------------------------------------------------------------- /onetab_to_markdown/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from onetab_to_markdown import App 5 | 6 | # paste links here: 7 | testlinks = ''' 8 | chrome-extension://klbibkeccnjlkjkiokjodocebajanakg/suspended.html#ttl=EbookFoundation%2Ffree-programming-books%3A%20Freely%20available%20programming%20books&uri=https://github.com/EbookFoundation/free-programming-books | EbookFoundation/free-programming-books: Freely available programming books 9 | https://example1.com | site title1 10 | https://example2.com | site title2 11 | ''' 12 | 13 | 14 | def test_init(): 15 | inst = App(testlinks) 16 | print(inst.in_list) 17 | 18 | 19 | def test_re_search(): 20 | inst = App(testlinks) 21 | for line in inst.in_list: 22 | print(inst.re_search(line)) 23 | 24 | 25 | def test_makelink(): 26 | inst = App(testlinks) 27 | inst.make_link() 28 | print(inst.out_list) 29 | 30 | 31 | def test_full(): 32 | inst = App(testlinks) 33 | mdlinks = inst.return_links() 34 | print('\n'*20, 'Finished!\n\n\n') 35 | # input('Press Enter to copy sorted links to your clipboard.') 36 | print(mdlinks) 37 | 38 | 39 | def run_tests(): 40 | # print(testlinks) 41 | # test_init() 42 | # test_re_search() 43 | # test_makelink() 44 | test_full() 45 | 46 | 47 | run_tests() 48 | -------------------------------------------------------------------------------- /org_tagged_md_links/org_tagged_md_links.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | help = """ 4 | Title: Markdown link organizer 5 | 6 | Follow instructions for copying text to clipboard. Output will be sent to clipboard. 7 | 8 | Example input: (for longer example, see org_tagged_md_links/test.py) 9 | # 170531 10 | 11 | Python / OOP 12 | * [PyBites: Code Challenge 20 - Object Oriented Programming Fun - Review](http://pybit.es/codechallenge20_review.html) | [http://pybit.es/codechallenge20_review.html](http://pybit.es/codechallenge20_review.html) 13 | 14 | - spaces between each link determine 'blocks' that belong together 15 | - non-special chars (*) are headers. 16 | """ 17 | 18 | import itertools 19 | from pprint import pprint 20 | import pyperclip 21 | import sys 22 | sys.path.insert(0, '../utils') 23 | import arg_utils 24 | 25 | 26 | class App: 27 | 28 | LOWEST_CHAR = '~' * 10 29 | 30 | def __init__(self, input_text): 31 | self.input_list = input_text.split('\n') 32 | # self.block_list = self.make_blocks(self.input_list) 33 | self.encoded_list = [] 34 | self.block_encoder() 35 | 36 | def block_encoder(self): 37 | '''Given bookmarks in markdown format (as list), a list of dicts with header and data is returned. 38 | ''' 39 | # Make blocks 40 | blocks = [list(g[1]) for g in itertools.groupby( 41 | self.input_list, key=lambda x: x.strip() != '') if g[0]] 42 | # pprint(blocks) 43 | 44 | for sublist in blocks: 45 | if sublist[0][0] == '#': 46 | continue 47 | elif sublist[0][0] != '*': 48 | self.encoded_list.append( 49 | {'header': sublist[0], 'data': sublist[1:]}) 50 | else: 51 | print() 52 | # add a custom header or hit enter to add a default LOWEST_CHAR 53 | for s in sublist: 54 | t = s.split('](') 55 | if t[1]: 56 | print('{}\n {}'.format(t[0][3:], t[1][:-1])) 57 | headerIn = self.headerInput() 58 | if headerIn != '': 59 | self.encoded_list.append( 60 | {'header': headerIn, 'data': sublist}) 61 | else: 62 | self.encoded_list.append( 63 | {'header': self.LOWEST_CHAR, 'data': sublist}) 64 | print() 65 | # print() #debug 66 | # sort and overwrite self.encoded_list... 67 | self.encoded_list = sorted( 68 | self.encoded_list, key=lambda k: k['header'].lower()) 69 | # pprint(self.encoded_list) 70 | 71 | @staticmethod 72 | def headerInput(): 73 | print('Type in a header - optional.') 74 | headerIn = input('> ') 75 | return headerIn 76 | 77 | def returnTags(self): 78 | headers = map(lambda item: item['header'], self.encoded_list) 79 | return ', '.join(sorted(list(set(headers)))) 80 | 81 | def return_sorted(self): 82 | # returns a string sorted data. make sure to run block_encoder first. 83 | sortedBlocks = [] 84 | for block in self.encoded_list: 85 | if block['header'] != self.LOWEST_CHAR: 86 | sortedBlocks.append(block['header']) 87 | for line in block['data']: 88 | if line.strip()[0] not in ['#']: 89 | sortedBlocks.append(line) 90 | sortedBlocks.append('') 91 | 92 | return '\n'.join(sortedBlocks) 93 | 94 | 95 | def main(): 96 | arg_utils.show_help_if_arg(sys.argv, help) 97 | print('Copy your titled links to clipboard, enter when done:') 98 | pause = input('> ') 99 | orgLinks = pyperclip.paste() 100 | o = App(orgLinks) 101 | fullText = o.returnTags() + '\n\n' + o.return_sorted() 102 | # pyperclip.copy(o.return_sorted()) 103 | pyperclip.copy(fullText) 104 | print('Your links were organized and copied to clipboard.') 105 | 106 | 107 | if __name__ == '__main__': 108 | main() 109 | -------------------------------------------------------------------------------- /org_tagged_md_links/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from pprint import pprint 5 | import org_tagged_md_links 6 | 7 | ORG_LINKS = ''' 8 | # MOBILE LINKS 9 | ## 2017 - May and June 10 | 11 | Priority 1: search !!! 12 | Chrome Extension 13 | Lots of JavaScript in here. 14 | 15 | # 5/24/2017 16 | 17 | * [Anki Manual](https://apps.ankiweb.net/docs/manual.html) 18 | 19 | A cool looking app / similar to my idea 20 | * [[macOS] Workspaces (beta) - an app that recreates working environment](https://www.reddit.com/r/webdev/comments/6ca3ts/macos_workspaces_beta_an_app_that_recreates/) | [https://www.reddit.com/r/webdev/comments/6ca3ts/macos_workspaces_beta_an_app_that_recreates/](https://www.reddit.com/r/webdev/comments/6ca3ts/macos_workspaces_beta_an_app_that_recreates/) 21 | 22 | # 170531 23 | 24 | Python / OOP 25 | * [PyBites: Code Challenge 20 - Object Oriented Programming Fun - Review](http://pybit.es/codechallenge20_review.html) | [http://pybit.es/codechallenge20_review.html](http://pybit.es/codechallenge20_review.html) 26 | 27 | JavaScript / FCC 28 | * [If I finish all certificates on freeCodeCamp, how long will I have to wait to start working on a n… by Quincy Larson](https://www.quora.com/If-I-finish-all-certificates-on-freeCodeCamp-how-long-will-I-have-to-wait-to-start-working-on-a-nonprofit-project/answer/Quincy-Larson?share=8f15f623&srid=4tK5) | [https://www.quora.com/If-I-finish-all-certificates-on-freeCodeCamp-how-long-will-I-have-to-wait-to-start-working-on-a-nonprofit-project/answer/Quincy-Larson?share=8f15f623&srid=4tK5](https://www.quora.com/If-I-finish-all-certificates-on-freeCodeCamp-how-long-will-I-have-to-wait-to-start-working-on-a-nonprofit-project/answer/Quincy-Larson?share=8f15f623&srid=4tK5) 29 | 30 | WebDev 31 | * [“My path to web development”](https://www.reddit.com/r/learnprogramming/comments/6djovj/my_path_to_web_development/) | [https://www.reddit.com/r/learnprogramming/comments/6djovj/my_path_to_web_development/](https://www.reddit.com/r/learnprogramming/comments/6djovj/my_path_to_web_development/) 32 | 33 | Regex resources 34 | * [If you struggle with REGULAR EXPRESSIONS, here is an awesome site that helps you understand them really quickly](https://www.reddit.com/r/learnprogramming/comments/6d93fs/if_you_struggle_with_regular_expressions_here_is/) | [https://www.reddit.com/r/learnprogramming/comments/6d93fs/if_you_struggle_with_regular_expressions_here_is/](https://www.reddit.com/r/learnprogramming/comments/6d93fs/if_you_struggle_with_regular_expressions_here_is/) 35 | 36 | Interesting reading. 37 | * [Looking for most enjoyed non-Python languages used by Python developers](https://www.reddit.com/r/Python/comments/6d5yn3/looking_for_most_enjoyed_nonpython_languages_used/) | [https://www.reddit.com/r/Python/comments/6d5yn3/looking_for_most_enjoyed_nonpython_languages_used/](https://www.reddit.com/r/Python/comments/6d5yn3/looking_for_most_enjoyed_nonpython_languages_used/) 38 | ''' 39 | 40 | 41 | def test_block_encoder(): 42 | # global ORG_LINKS 43 | text = ORG_LINKS 44 | app = org_tagged_md_links.App(text) 45 | pprint(app.encoded_list) 46 | 47 | 48 | def test_return_tags(): 49 | # global ORG_LINKS 50 | text = ORG_LINKS 51 | app = org_tagged_md_links.App(text) 52 | print(app.returnTags()) 53 | 54 | 55 | def test_return_sorted(): 56 | # global ORG_LINKS 57 | text = ORG_LINKS 58 | app = org_tagged_md_links.App(text) 59 | print(app.return_sorted()) 60 | 61 | 62 | def test_main(): 63 | org_tagged_md_links.main() 64 | 65 | 66 | def run_tests(): 67 | # test_block_encoder() 68 | # test_return_tags() 69 | # test_return_sorted() 70 | test_main() 71 | 72 | 73 | run_tests() 74 | -------------------------------------------------------------------------------- /setup/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | 5 | userHome = os.path.expanduser('~') 6 | config_name = 'config.json' 7 | 8 | config_template = { 9 | "directories": { 10 | "bookmarksRootDir": os.path.join(userHome, 'Desktop', 'bookmarksBackups'), 11 | "chromeJSON": os.path.join("chrome_json"), 12 | "chromeMD": os.path.join("chrome_md"), 13 | "firefoxJson": os.path.join( 14 | userHome, "Library/Application Support/Google/Chrome/Default/Bookmarks"), 15 | "mobileLinksDir": "mobileLinks", 16 | }, 17 | 18 | "filenames": { 19 | "chr_md_file_prefix": "chrome.md" 20 | }, 21 | 22 | "markdownFormat": "standard" 23 | } 24 | 25 | 26 | def write_config_file(new_filename): 27 | with open(new_filename, 'w') as new_file: 28 | new_file.write( 29 | json.dumps(config_template, indent=4) 30 | ) 31 | print('File successfully written:', new_filename) 32 | 33 | 34 | def file_exists(filePath): 35 | if os.path.exists(filePath): 36 | raise OSError(filePath + ' file exists.') 37 | 38 | 39 | def main(): 40 | config_main_directory = os.path.join('..', config_name) 41 | file_exists(config_main_directory) 42 | write_config_file(config_main_directory) 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scraggo/bookmarks-markdown-utils/2fa8a9e66f948b5d486cb9927106b36ecaa68ac4/utils/__init__.py -------------------------------------------------------------------------------- /utils/arg_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | # "scriptName": [ "path", "fileName" ] 6 | argPathMap = { 7 | 'chrome_to_markdown': ['chrome_to_markdown', 'main.py'], 8 | 'combine_files': ['combine_files', 'combine_files.py'], 9 | 'delete_leading_text': ['delete_leading_text', 'delete_leading_text.py'], 10 | 'find_replace_chrome_html': ['find_replace', 'find_replace_chrome_html.py'], 11 | 'find_replace_evernote_html': ['find_replace', 'find_replace_evernote_html.py'], 12 | 'find_replaceJSON_FF_01': ['find_replace', 'find_replaceJSON_FF_01.py'], 13 | 'find_replaceJSON_FF_02': ['find_replace', 'find_replaceJSON_FF_02.py'], 14 | 'makelink_evernote_md': ['make_link', 'makelink_evernote_md.py'], 15 | 'makelink_url': ['make_link', 'makelink_url.py'], 16 | 'onetab_to_markdown': ['onetab_to_markdown', 'onetab_to_markdown.py'], 17 | 'org_tagged_md_links': ['org_tagged_md_links', 'org_tagged_md_links.py'], 18 | 'setup': ['setup', 'setup.py'], 19 | 'visit_and_tag_md_links': ['visit_and_tag_md_links', 'main.py'] 20 | } 21 | 22 | help_text = 'Valid scripts are:\n ' + \ 23 | "\n ".join(map(lambda x: "'" + x + "'", argPathMap.keys())) 24 | 25 | 26 | def show_help_and_exit(help=help_text): 27 | print(help) 28 | sys.exit() 29 | 30 | 31 | def show_help_if_arg(args, helpText): 32 | if len(args) > 1 and args[1] == '-h': 33 | show_help_and_exit(helpText) 34 | 35 | 36 | def handle_no_match(val0=None): 37 | if val0 == None: 38 | print('Script not found.') 39 | show_help_and_exit() 40 | 41 | 42 | def safe_get_path(key): 43 | values = argPathMap.get(key, [None, None]) 44 | handle_no_match(values[0]) 45 | return values 46 | 47 | 48 | def get_rest_of_args(args, startIdx=0): 49 | if len(args) < startIdx + 1: 50 | return [] 51 | return args[startIdx:] 52 | 53 | 54 | def execute_args(args): 55 | '''assuming args[0] is script name...''' 56 | if len(args) < 1: 57 | handle_no_match() 58 | if args[0] == '-h': 59 | show_help_and_exit() 60 | 61 | scriptName = args[0] 62 | path, fileName = safe_get_path(scriptName) 63 | restOfArgs = get_rest_of_args(args, 1) 64 | 65 | os.chdir(path) 66 | subprocess.run( 67 | ['python', fileName] + restOfArgs) 68 | -------------------------------------------------------------------------------- /utils/date_append.py: -------------------------------------------------------------------------------- 1 | ''' 2 | date_append.py 3 | returns a filepath + today's date in format -yymmdd (example: 170612 for July 12, 4 | 2017. 5 | file can be non-existing, but the directory must be valid. 6 | Example output: /Users/you/test-170612.py 7 | ''' 8 | import os 9 | from datetime import datetime as d 10 | 11 | def todayDate(): 12 | '''return today's date in format 170612 (yymmdd) 13 | ''' 14 | return d.now().strftime("%y%m%d") 15 | 16 | def date_append_filename(filename): 17 | '''filename is renamed without taking extension into account 18 | ''' 19 | return ''.join([filename, '-', todayDate()]) 20 | 21 | def date_append(filePath): 22 | '''param::filePath - a string of a valid filePath for a proposed file to 23 | create or overwrite. Example: '/my/valid/directory/proposed-file-name.txt' 24 | Error thrown if the directory isn't valid. 25 | Best results if extension is specified properly. Example: '.txt' 26 | ''' 27 | if os.path.isdir(os.path.dirname(filePath)): 28 | filename, file_extension = os.path.splitext(filePath) 29 | return ''.join([filename, '-', todayDate(), file_extension]) 30 | else: 31 | raise OSError('Directory ' + os.path.dirname(filePath) + ' does not exist.') 32 | 33 | #Test 34 | # print(date_append('/Chrome_Bookmarks_backupDEC.py')) #not a file / full path 35 | 36 | # print(date_append('/Users/davecohen/Dropbox/Notes/-BookmarkProject/PyBookmarkScripts/DEC-Chrome2Markdown/old')) #is a directory 37 | 38 | # print(date_append('/Users/davecohen/Dropbox/Notes/-BookmarkProject/PyBookmarkScripts/DEC-Chrome2Markdown/test.what.py')) #is a valid directory / filename -------------------------------------------------------------------------------- /utils/file_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import shutil 6 | import sys 7 | # from chr_config import directories 8 | 9 | # this import only works if you're in this directory 10 | from get_config import get_json_config 11 | 12 | directories = get_json_config()['directories'] 13 | 14 | ''' 15 | Calling shutil.move(source, destination) will move the file or folder at the path source to the path destination and will return a string of the absolute path of the new location. 16 | 17 | If destination points to a folder, the source file gets moved into destination and keeps its current filename. For example, enter the following into the interactive shell: 18 | ''' 19 | 20 | 21 | class FileUtils: 22 | # def __init__(self, input_file_path): 23 | # self.input_file_path = input_file_path 24 | 25 | def fileExists(self, filePath): 26 | if os.path.exists(filePath): 27 | raise OSError(filePath + ' file exists.') 28 | 29 | def join_path(self, top_dir, root_dir=None): 30 | if not root_dir: 31 | root_dir = directories['bookmarksRootDir'] 32 | return os.path.join(root_dir, top_dir) 33 | 34 | def copy_file(self, file_path, new_dir): 35 | fileName = os.path.split(file_path)[1] 36 | self.fileExists(os.path.join(new_dir, fileName)) 37 | return shutil.copy(file_path, new_dir) 38 | 39 | # def rename_file(self, input_file_path, new_file_name): 40 | # output = self.join_path(directories[top_dir]) 41 | # self.fileExists(output) 42 | # # print(input_file_path, output) 43 | # shutil.copy(input_file_path, output) 44 | # if new_file_name: 45 | # new_output = os.path.join(output, new_file_name) 46 | # self.fileExists(new_output) 47 | # return os.rename(output, new_output) 48 | 49 | def move_to_mobileLinksDir(self, input_file_path): 50 | output = self.join_path(directories['mobileLinksDir']) 51 | return shutil.move(input_file_path, output) 52 | 53 | def copy_to_chromeMarkdownBackupsDir(self, input_file_path): 54 | output = self.join_path(directories['chromeMD']) 55 | return shutil.copy(input_file_path, output) 56 | 57 | def copy_to_chrJsonBackupsDir(self, input_file_path, new_file_name): 58 | newDest = self.join_path(directories['chromeJSON']) 59 | # oldFileName = os.path.split(input_file_path)[1] 60 | # self.copy_file(input_file_path, newDest) 61 | oldNameNewDest = self.copy_file(input_file_path, newDest) 62 | newFilePath = os.path.join(newDest, new_file_name) 63 | os.rename(oldNameNewDest, newFilePath) 64 | return newFilePath 65 | -------------------------------------------------------------------------------- /utils/get_config.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def get_json_config(): 4 | with open("../config.json", "r") as read_file: 5 | return json.load(read_file) 6 | -------------------------------------------------------------------------------- /utils/pyperclip_interface.py: -------------------------------------------------------------------------------- 1 | import pyperclip 2 | 3 | class PyperInterface: 4 | def __init__(self, in_message, out_message): 5 | self.in_message = in_message 6 | self.out_message = out_message 7 | self.in_data = '' 8 | self.out_data = '' 9 | 10 | def get_clipboard(self, pause = True): 11 | print(self.in_message) 12 | if pause: 13 | input('> ') 14 | self.in_data = pyperclip.paste() 15 | return self.in_data 16 | 17 | def send_to_clipboard(self, data): 18 | pyperclip.copy(data) 19 | self.out_data = data 20 | print(self.out_message) 21 | -------------------------------------------------------------------------------- /visit_and_tag_md_links/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scraggo/bookmarks-markdown-utils/2fa8a9e66f948b5d486cb9927106b36ecaa68ac4/visit_and_tag_md_links/__init__.py -------------------------------------------------------------------------------- /visit_and_tag_md_links/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import json 7 | from visit_and_tag_md_links import App 8 | 9 | # this import only works if you're in this directory 10 | sys.path.insert(0, '../utils') 11 | import file_utils 12 | from get_config import get_json_config 13 | 14 | 15 | def debug(obj): 16 | '''{name: data}''' 17 | for name in obj.keys(): 18 | print('{}: {}'.format(name, obj[name])) 19 | 20 | 21 | def get_file_arr(file_path): 22 | with open(file_path) as f: 23 | return f.readlines() 24 | 25 | 26 | def get_timestamp(): 27 | import time 28 | time.sleep(.01) 29 | return str(int(time.time() * 1000))[4:] 30 | 31 | 32 | def get_new_filepath(old_file): 33 | import os 34 | filename, file_extension = os.path.splitext(old_file) 35 | new_filename = filename + get_timestamp() + file_extension 36 | return new_filename 37 | 38 | 39 | def write_sorted_file(new_filename, header_str, links_list): 40 | with open(new_filename, 'w') as new_file: 41 | new_file.write(header_str) 42 | for obj in links_list: 43 | new_file.write('\n\n' + obj['header'] + '\n') 44 | for line in obj['data']: 45 | new_file.write(line.strip() + '\n') 46 | print('Organized links:\n\t', new_filename) 47 | 48 | 49 | def write_remaining(new_filename, remaining_list): 50 | with open(new_filename, 'w') as new_file: 51 | for line in remaining_list: 52 | new_file.write(line) 53 | print('Unorganized links:\n\t', new_filename) 54 | 55 | 56 | def main(): 57 | if len(sys.argv) < 2: 58 | sys.exit('error: Please enter a text/markdown filepath.') 59 | md_file = sys.argv[1] 60 | links = App(get_file_arr(md_file)) 61 | links.block_encoder() 62 | headerList = links.get_sorted_headers(string=True) 63 | organizedLinks = links.organized_list 64 | newFileName = get_new_filepath(md_file) 65 | 66 | write_sorted_file(newFileName, headerList, organizedLinks) 67 | 68 | newFileName2 = get_new_filepath(md_file) 69 | remainingList = links.remaining_list 70 | write_remaining(newFileName2, remainingList) 71 | # For now, inform user he can delete original file. 72 | print('You may delete:\n\t', md_file) 73 | 74 | 75 | if __name__ == '__main__': 76 | main() 77 | -------------------------------------------------------------------------------- /visit_and_tag_md_links/main_pyperclip.py: -------------------------------------------------------------------------------- 1 | import pyperclip 2 | 3 | from visit_and_tag_md_links import App 4 | 5 | def main(): 6 | print('Copy your markdown links to clipboard, enter when done:') 7 | input('> ') 8 | todoTxt = pyperclip.paste() 9 | todoArray = todoTxt.split('\n') 10 | links = App(todoArray) 11 | links.block_encoder() 12 | headerList = links.get_sorted_headers() 13 | allLinks = links.return_sorted() 14 | pyperclip.copy(headerList + '\n\n' + allLinks) 15 | print('\n'*12) 16 | print('Finished!') 17 | print('Sorted links were copied to your clipboard.') 18 | 19 | if __name__ == '__main__': 20 | main() -------------------------------------------------------------------------------- /visit_and_tag_md_links/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from visit_and_tag_md_links import App 5 | 6 | todoTxt = \ 7 | ''' 8 | * [Mobile-Keep]* [Other Bookmarks]* [---MOBILE-BOOKMARKS](about:blank) | [about:blank](about:blank) 9 | * [When am I experienced enough to apply for internships?](https://www.reddit.com/r/cscareerquestions/comments/6o3he4/when_am_i_experienced_enough_to_apply_for/) | [https://www.reddit.com/r/cscareerquestions/comments/6o3he4/when_am_i_experienced_enough_to_apply_for/](https://www.reddit.com/r/cscareerquestions/comments/6o3he4/when_am_i_experienced_enough_to_apply_for/) 10 | * [What are you guys using for styling form controls nowadays?](https://www.reddit.com/r/web_design/comments/6pphww/what_are_you_guys_using_for_styling_form_controls/) | [https://www.reddit.com/r/web_design/comments/6pphww/what_are_you_guys_using_for_styling_form_controls/](https://www.reddit.com/r/web_design/comments/6pphww/what_are_you_guys_using_for_styling_form_controls/) 11 | * [O'Reilly is offering ebooks about UX, Data, IoT...](https://www.reddit.com/r/webdev/comments/6pgjg4/oreilly_is_offering_ebooks_about_ux_data_iot/) | [https://www.reddit.com/r/webdev/comments/6pgjg4/oreilly_is_offering_ebooks_about_ux_data_iot/](https://www.reddit.com/r/webdev/comments/6pgjg4/oreilly_is_offering_ebooks_about_ux_data_iot/) 12 | * [SPAs vs MPAs/MVC - Are Single Page Apps always better?](https://www.reddit.com/r/webdev/comments/6phqhe/spas_vs_mpasmvc_are_single_page_apps_always_better/) | [https://www.reddit.com/r/webdev/comments/6phqhe/spas_vs_mpasmvc_are_single_page_apps_always_better/](https://www.reddit.com/r/webdev/comments/6phqhe/spas_vs_mpasmvc_are_single_page_apps_always_better/) 13 | * [Rotten oranges problem, http://www.geeksforgeeks.org/minimum-time-required-so-that-all-oranges-become-rotten/](https://www.reddit.com/r/Python/comments/6vk31h/rotten_oranges_problem/) | [https://www.reddit.com/r/Python/comments/6vk31h/rotten_oranges_problem/](https://www.reddit.com/r/Python/comments/6vk31h/rotten_oranges_problem/) 14 | ''' 15 | 16 | def test_visit_link(): 17 | # global todoTxt 18 | t = todoTxt.split('\n') 19 | test = App(t) 20 | for line in t: 21 | print(line) 22 | test.visit_link(line) 23 | 24 | def run_test_suite(): 25 | test_visit_link() 26 | 27 | run_test_suite() -------------------------------------------------------------------------------- /visit_and_tag_md_links/visit_and_tag_md_links.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on 7/25/17 5 | 6 | @author: davecohen 7 | 8 | Title: Markdown link organizer 9 | Given list of markdown links, each link is visited in your default browser, 10 | then you can "tag" the link in the terminal. 11 | output format from my markdown encoder 12 | see example below for the expected in format 13 | - no spaces between each link. 14 | """ 15 | # import itertools 16 | # from pprint import pprint 17 | import re, webbrowser 18 | 19 | class App: 20 | 21 | # linkRegex = re.compile(r'(https?\:\/\/[^)]*)') 22 | linkRegex = re.compile(r'(\()(https?\:\/\/[^)]*)(\))') 23 | 24 | def __init__(self, in_list): 25 | self.in_list = in_list 26 | self.data_blocks = [] 27 | self.organized_list = [] 28 | self.header_map = {} 29 | self.remaining_list = [] 30 | self.DEFAULT_CHAR = '~~~NoTag~~~' 31 | 32 | def block_encoder(self): 33 | '''Given bookmarks in markdown format (as list), a list of dicts with header and data is returned. 34 | ''' 35 | # blocks = [list(g[1]) for g in itertools.groupby(todoArray, key= lambda x: x.strip() != '') if g[0]] 36 | 37 | line_num_w_data = -1 38 | line_num = -1 39 | for line in self.in_list: 40 | line_num += 1 41 | line = line.strip() 42 | if len(line) <= 0: 43 | continue 44 | if line[0] == '*': 45 | line_num_w_data += 1 46 | self.visit_link(line) 47 | print(line) 48 | #add a custom header or hit enter to add DEFAULT 49 | headerIn = self.header_input() 50 | if headerIn.lower() == '.quit': 51 | # append remaining lines to data blocks 52 | # self.append_remaining_lines(line_num_w_data, line_num) 53 | self.get_remaining_lines(line_num) 54 | break 55 | elif headerIn == '': 56 | self.update_header_and_data(self.header_map, self.DEFAULT_CHAR, self.data_blocks, line) 57 | else: 58 | self.update_header_and_data(self.header_map, headerIn, self.data_blocks, line) 59 | 60 | self.organized_list = self.sort_data_list(self.data_blocks) 61 | 62 | @staticmethod 63 | def pprintObj(obj): 64 | import pprint 65 | pprint.pprint(obj) 66 | 67 | # def append_remaining_lines(self, line_num_w_data, line_num): 68 | # emptyHeaderIdx = self.header_map.get(self.DEFAULT_CHAR, -1) 69 | # if emptyHeaderIdx == -1: 70 | # self.data_blocks.append({ 71 | # 'header': self.DEFAULT_CHAR, 72 | # 'data': [] 73 | # }) 74 | # emptyHeaderIdx = len(self.data_blocks) - 1 75 | # # print(emptyHeaderIdx, line_num_w_data) 76 | # # print(self.in_list[line_num:line_num+3]) 77 | # # print(self.data_blocks[emptyHeaderIdx]) 78 | # self.data_blocks[emptyHeaderIdx]['data'] += self.in_list[line_num:] 79 | 80 | def get_remaining_lines(self, line_num): 81 | self.remaining_list = self.in_list[line_num:] 82 | 83 | @staticmethod 84 | def update_header_and_data(obj, header, dataList, data): 85 | if obj.get(header, False) is False: 86 | idx = len(dataList)#last available idx 87 | obj[header] = idx 88 | newObj = { 89 | 'header': header, 90 | 'data': [data] 91 | } 92 | dataList.append(newObj) 93 | else: 94 | idx = obj[header] 95 | dataList[idx]['data'].append(data) 96 | 97 | @staticmethod 98 | def sort_data_list(data_list): 99 | return sorted(data_list, key=lambda k: k['header'].lower()) 100 | 101 | def visit_link(self, str_url): 102 | # FYI - link regex - (https?\:\/\/[^)]*) 103 | # linkRegex = re.compile(r'(https?\:\/\/[^)]*)') 104 | link_mo = self.linkRegex.search(str_url) 105 | if link_mo: 106 | #open link if found 107 | webbrowser.open(link_mo.group(2)) 108 | 109 | @staticmethod 110 | def header_input(): 111 | print('Type in a header - optional. \'.quit\' to stop.') 112 | headerIn = input('> ').lower() 113 | return headerIn 114 | 115 | 116 | def print_sorted(self): 117 | #print sorted data 118 | for block in self.data_blocks: 119 | if block['header'] != 'zzzzz': 120 | print(block['header']) 121 | print(block['data']) 122 | # for line in block['data']: 123 | # if line.strip()[0] not in ['#']: 124 | # print(line) 125 | print() 126 | 127 | def return_sorted(self, string = True): 128 | sorted_r = [] 129 | 130 | for block in self.data_blocks: 131 | if block['header'] != 'zzzzz': 132 | sorted_r.append(block['header']) 133 | sorted_r.append(block['data']) 134 | # for line in block['data']: 135 | # if line.strip()[0] not in ['#']: 136 | # print(line) 137 | sorted_r.append('') 138 | 139 | if string: 140 | return '\n'.join(sorted_r) 141 | else: 142 | return sorted_r 143 | 144 | def get_sorted_headers(self, string = True): 145 | # headerList = list(set(self.organized_list)) 146 | headerList = sorted(self.header_map.keys()) 147 | if string: 148 | return ', '.join(headerList) 149 | else: 150 | return headerList 151 | 152 | 153 | --------------------------------------------------------------------------------