├── .gitignore ├── README.md ├── img ├── img1.jpg └── img2.jpg ├── javascript └── readme_browser.js ├── readme_browser ├── cache.py ├── main.py ├── options.py ├── readme_files.py ├── tools.py └── wiki.py ├── scripts └── readme_browser.py └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode 3 | .directory 4 | cache 5 | wiki 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Readme browser 2 | 3 | This is an extension for [AUTOMATIC1111/stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui). It allows user to view readme files of extensions right inside webui, in `Extensions` -> `Readme files` tab 4 | 5 | ![](/img/img1.jpg) 6 | 7 | Supported things: 8 | 1. Local media (images, video, or just local files) 9 | 1. Local nested .md files, e.g. docs or the readme in other languages 10 | 1. Local anchors 11 | 1. External links and media work as usual, and requires the Internet connection, of course 12 | 1. GitHub Wiki of extension, if it's mentioned in its readme 13 | 14 | You can adjust few settings. The defaults are in the screenshot: 15 | 16 | ![](/img/img2.jpg) 17 | -------------------------------------------------------------------------------- /img/img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-and-ray/sd-webui-readme-browser/19d6e6859090db7c89b137ea0715db3c280e2132/img/img1.jpg -------------------------------------------------------------------------------- /img/img2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/light-and-ray/sd-webui-readme-browser/19d6e6859090db7c89b137ea0715db3c280e2132/img/img2.jpg -------------------------------------------------------------------------------- /javascript/readme_browser.js: -------------------------------------------------------------------------------- 1 | 2 | var readme_browser_subFilePath = undefined; 3 | 4 | function readme_browser_openSubFile_(dummy, extPath, extName) { 5 | return [readme_browser_subFilePath, extPath, extName]; 6 | } 7 | 8 | 9 | function readme_browser_openSubFile(filePath) { 10 | readme_browser_subFilePath = decodeURI(filePath); 11 | let button = gradioApp().getElementById('readme_browser_openSubFileButton'); 12 | button.click(); 13 | } 14 | 15 | 16 | var readme_browser_wikiName = undefined; 17 | var readme_browser_filePath = undefined; 18 | 19 | function readme_browser_openWiki_(dummy1, dummy2) { 20 | return [readme_browser_wikiName, readme_browser_filePath]; 21 | } 22 | 23 | function readme_browser_openWiki(wikiName, filePath) { 24 | readme_browser_wikiName = decodeURI(wikiName); 25 | readme_browser_filePath = decodeURI(filePath); 26 | let button = gradioApp().getElementById('readme_browser_openWikiButton'); 27 | button.click(); 28 | } 29 | 30 | 31 | function readme_browser_alreadyHasAnchor(h) { 32 | let elem = h.previousSibling; 33 | if (!elem?.classList?.contains('readme_browser_h_anchor')) return false; 34 | return true; 35 | } 36 | 37 | 38 | function readme_browser_afterRender() { 39 | let file = gradioApp().getElementById('readme_browser_file'); 40 | let hElements = [...file.querySelectorAll("h1, h2, h3, h4, h5, h6")]; 41 | let anchorNumbers = {}; 42 | hElements.forEach((h) => { 43 | if (readme_browser_alreadyHasAnchor(h)) return; 44 | if (!h.innerHTML) return; 45 | let anchor = document.createElement('a'); 46 | let anchorID = h.innerText.toLowerCase().replaceAll(' ', '-').replace(/[^a-zA-Z0-9-_]/g, ''); 47 | if (anchorID in anchorNumbers) { 48 | let key = anchorID; 49 | anchorID += '-' + anchorNumbers[key]; 50 | anchorNumbers[key] += 1; 51 | } else { 52 | anchorNumbers[anchorID] = 1; 53 | } 54 | anchor.setAttribute('id', anchorID); 55 | anchor.classList.add('readme_browser_h_anchor'); 56 | h.parentNode.insertBefore(anchor, h); 57 | }); 58 | 59 | 60 | let aElements = [...file.getElementsByTagName('a')]; 61 | aElements.forEach((a) => { 62 | if (!a.href) return; 63 | const url = new URL(a.href); 64 | if (url.origin === window.location.origin && a.href.indexOf('#') !== -1) { 65 | a.setAttribute('target', ''); 66 | return; 67 | } 68 | const prefix = 'readme_browser_javascript_'; 69 | const prefixIndex = a.href.indexOf(prefix); 70 | if (prefixIndex !== -1) { 71 | let onClick = a.href.slice(prefixIndex + prefix.length); 72 | onClick = decodeURI(onClick).replaceAll("%2C", ","); 73 | a.setAttribute('onclick', onClick); 74 | a.setAttribute('target', ''); 75 | a.href = '#readme_browser_top_anchor'; 76 | } 77 | }); 78 | } 79 | 80 | -------------------------------------------------------------------------------- /readme_browser/cache.py: -------------------------------------------------------------------------------- 1 | import os, hashlib, time 2 | import urllib.request 3 | from threading import Thread 4 | from readme_browser.options import getCacheLocation 5 | from readme_browser.tools import hasAllowedExt 6 | 7 | opener = urllib.request.build_opener() 8 | opener.addheaders = [('User-agent', 'Mozilla/5.0')] 9 | urllib.request.install_opener(opener) 10 | 11 | 12 | def needCacheURL(url: str): 13 | url = url.lower() 14 | 15 | if 'github.com' in url and '/assets/' in url: 16 | return True 17 | if 'github.com' in url and '/blob/' in url and hasAllowedExt(url): 18 | return True 19 | if 'githubusercontent.com' in url: 20 | return True 21 | if 'i.imgur.com' in url: 22 | return True 23 | return False 24 | 25 | 26 | def cache(url: str, extName: str) -> str|None: 27 | if '?' in url: 28 | url = url.removesuffix('?' + url.split('?')[-1]) 29 | if not needCacheURL(url): 30 | return None 31 | 32 | cachedFile = None 33 | 34 | try: 35 | name = url.split('/')[-1] 36 | dirHash = hashlib.md5(url.removesuffix(name).encode('utf-8')).hexdigest()[:6] 37 | outPath = os.path.join(getCacheLocation(), extName, dirHash, name) 38 | os.makedirs(os.path.dirname(outPath), exist_ok=True) 39 | if os.path.exists(outPath): 40 | cachedFile = outPath 41 | else: 42 | def func(): 43 | try: 44 | nonlocal url 45 | time.sleep(1) 46 | url += '?raw=true' 47 | urllib.request.urlretrieve(url, outPath) 48 | print(f'readme_browser cached file {url}, extName = {extName}') 49 | except Exception as e: 50 | # print(e) 51 | pass 52 | Thread(target=func).start() 53 | 54 | except Exception as e: 55 | print(f'Error while caching file {url}, extName = {extName}') 56 | print(e) 57 | 58 | return cachedFile 59 | 60 | -------------------------------------------------------------------------------- /readme_browser/main.py: -------------------------------------------------------------------------------- 1 | import os, time 2 | import urllib.parse 3 | from pathlib import Path 4 | from threading import Thread 5 | import gradio as gr 6 | from readme_browser.tools import (getURLsFromFile, isLocalURL, isAnchor, isMarkdown, 7 | makeOpenRepoLink, JS_PREFIX, replaceURLInFile, saveLastCacheDatetime, hasAllowedExt, 8 | makeAllMarkdownFilesList, SPECIAL_EXTENSION_NAMES, enoughTimeLeftForCache, makeFileIndex, 9 | addJumpAnchors, 10 | ) 11 | 12 | from readme_browser.options import needCache 13 | from readme_browser.cache import cache 14 | from readme_browser import readme_files 15 | from readme_browser.wiki import isWikiURL, getLocalWikiURL, makeDummySidebar, getWikiFilePath 16 | 17 | 18 | def renderMarkdownFile(filePath: str, extDir: str, extName: str): 19 | isWiki = extName.startswith('wiki - ') 20 | 21 | with open(filePath, mode='r', encoding="utf-8-sig") as f: 22 | file = f.read() 23 | 24 | if isWiki: 25 | footerPath = os.path.join(os.path.dirname(filePath), '_Footer.md') 26 | if os.path.exists(footerPath): 27 | with open(footerPath, mode='r', encoding="utf-8-sig") as f: 28 | file += '\n\n' + f.read() 29 | 30 | file = addJumpAnchors(file) 31 | file += makeFileIndex(file) 32 | 33 | if isWiki: 34 | sidebarPath = os.path.join(os.path.dirname(filePath), '_Sidebar.md') 35 | if os.path.exists(sidebarPath): 36 | with open(sidebarPath, mode='r', encoding="utf-8-sig") as f: 37 | sidebar = f.read() 38 | else: 39 | sidebar = makeDummySidebar(os.path.dirname(filePath)) 40 | 41 | file += '\n\n-----------\nSidebar\n-------------\n\n' + sidebar 42 | else: 43 | allMarkdownFilesList = makeAllMarkdownFilesList(extDir) 44 | if allMarkdownFilesList: 45 | file += '\n\n-----------\nAll markdown files\n-------------\n\n' + allMarkdownFilesList 46 | 47 | 48 | for url in getURLsFromFile(file): 49 | originalURL = url 50 | replacementUrl = None 51 | if JS_PREFIX in originalURL: 52 | file = file.replace(originalURL, "***") 53 | continue 54 | 55 | if 'github.com' in url and '/blob/' in url: 56 | url = url.split('/blob/')[-1] 57 | url = url.removeprefix(url.split('/')[0]) 58 | if '?' in url: 59 | url = url.removesuffix('?' + url.split('?')[-1]) 60 | 61 | if isLocalURL(url): 62 | if isAnchor(url): continue 63 | if '#' in url: 64 | url = url.removesuffix('#' + url.split('#')[-1]) 65 | if isWiki and not hasAllowedExt(url): 66 | url += '.md' 67 | 68 | if url[0] == '/': 69 | urlFullPath = os.path.join(extDir, url[1:]) 70 | else: 71 | urlFullPath = os.path.join(os.path.dirname(filePath), url) 72 | 73 | if os.path.exists(urlFullPath): 74 | if isMarkdown(url): 75 | replacementUrl = f"{JS_PREFIX}readme_browser_openSubFile('{urlFullPath}')" 76 | else: 77 | replacementUrl = f'file={urlFullPath}' 78 | 79 | elif isWikiURL(url, extName): 80 | replacementUrl = getLocalWikiURL(url) 81 | 82 | if replacementUrl is None and needCache() and not isLocalURL(originalURL): 83 | cachedFile = cache(originalURL, extName) 84 | if cachedFile: 85 | replacementUrl = f'file={cachedFile}' 86 | 87 | if replacementUrl is not None: 88 | replacementUrl = urllib.parse.quote(replacementUrl) 89 | file = replaceURLInFile(file, originalURL, replacementUrl) 90 | 91 | if enoughTimeLeftForCache(extName): 92 | saveLastCacheDatetime(extName) 93 | return file 94 | 95 | 96 | def selectExtension(extName: str): 97 | if extName not in readme_files.readmeFilesByExtName.keys(): 98 | return "", "", "", "" 99 | data = readme_files.readmeFilesByExtName[extName] 100 | file = renderMarkdownFile(data.filePath, data.extPath, extName) 101 | openRepo = makeOpenRepoLink(data.extPath) 102 | return file, data.extPath, extName, openRepo 103 | 104 | 105 | def openSubFile(filePath: str, extPath: str, extName: str): 106 | file = renderMarkdownFile(filePath, extPath, extName) 107 | return file 108 | 109 | 110 | def openWiki(wikiName, filePath): 111 | filePath = getWikiFilePath(wikiName, filePath) 112 | dirPath = os.path.dirname(filePath) 113 | file = renderMarkdownFile(filePath, dirPath, f'wiki - {wikiName}') 114 | openRepo = makeOpenRepoLink(dirPath) 115 | return file, filePath, wikiName, openRepo 116 | 117 | 118 | 119 | markdownFile = gr.Markdown("", elem_classes=['readme-browser-file'], elem_id='readme_browser_file') 120 | openRepo = gr.Markdown("", elem_classes=['readme-browser-open-repo'], elem_id='readme_browser_open_repo') 121 | 122 | def getTabUI(): 123 | readme_files.initReadmeFiles() 124 | 125 | with gr.Blocks() as tab: 126 | dummy_component = gr.Textbox("", visible=False) 127 | extPath = gr.Textbox("", visible=False) 128 | extName = gr.Textbox("", visible=False) 129 | 130 | with gr.Row(): 131 | selectedExtension = gr.Dropdown( 132 | label="Extension", 133 | value="", 134 | choices=[""] + list(readme_files.readmeFilesByExtName.keys()) 135 | ) 136 | selectButton = gr.Button('Select') 137 | selectButton.click( 138 | fn=selectExtension, 139 | inputs=[selectedExtension], 140 | outputs=[markdownFile, extPath, extName, openRepo], 141 | ).then( 142 | fn=None, 143 | _js='readme_browser_afterRender', 144 | ) 145 | 146 | with gr.Row(): 147 | markdownFile.render() 148 | 149 | with gr.Row(): 150 | openRepo.render() 151 | 152 | openSubFileButton = gr.Button("", visible=False, elem_id="readme_browser_openSubFileButton") 153 | openSubFileButton.click( 154 | fn=openSubFile, 155 | _js="readme_browser_openSubFile_", 156 | inputs=[dummy_component, extPath, extName], 157 | outputs=[markdownFile] 158 | ).then( 159 | fn=None, 160 | _js='readme_browser_afterRender', 161 | ) 162 | 163 | openWikiButton = gr.Button("", visible=False, elem_id="readme_browser_openWikiButton") 164 | openWikiButton.click( 165 | fn=openWiki, 166 | _js="readme_browser_openWiki_", 167 | inputs=[dummy_component, dummy_component], 168 | outputs=[markdownFile, extPath, extName, openRepo] 169 | ).then( 170 | fn=None, 171 | _js='readme_browser_afterRender', 172 | ) 173 | 174 | return tab 175 | 176 | 177 | def cacheAll(demo, app): 178 | def func(): 179 | for extName, data in readme_files.readmeFilesByExtName.items(): 180 | if not enoughTimeLeftForCache(extName): 181 | continue 182 | if extName in SPECIAL_EXTENSION_NAMES: 183 | continue 184 | try: 185 | for mdFile in Path(data.extPath).rglob('*.md'): 186 | _ = renderMarkdownFile(mdFile, data.extPath, extName) 187 | except Exception as e: 188 | print(f'Error on creating cache on startup, data.extPath = {data.extPath}') 189 | print(e) 190 | time.sleep(60) 191 | 192 | Thread(target=func).start() 193 | -------------------------------------------------------------------------------- /readme_browser/options.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import gradio as gr 4 | from modules import shared 5 | 6 | def needUseOnUICallback(): 7 | res : str = shared.opts.data.get("readme_browser_tab_location", "Extensions -> Readme files") 8 | return res == "Readme files" 9 | 10 | 11 | def needHideDisabledExtensions(): 12 | res : bool = shared.opts.data.get("readme_browser_hide_disabled_extensions", False) 13 | return res 14 | 15 | def needCache(): 16 | res : bool = shared.opts.data.get("readme_browser_need_cache", True) 17 | return res 18 | 19 | def needCacheOnStartup(): 20 | if not needCache(): 21 | return False 22 | res : bool = shared.opts.data.get("readme_browser_need_cache_on_startup", False) 23 | return res 24 | 25 | EXT_ROOT_DIRECTORY = str(Path(__file__).parent.parent.absolute()) 26 | DEFAULT_CACHE_LOCATION = os.path.join(EXT_ROOT_DIRECTORY, 'cache') 27 | 28 | def getCacheLocation(): 29 | res : str = shared.opts.data.get("readme_browser_cache_location", "") 30 | if res == "": 31 | res = DEFAULT_CACHE_LOCATION 32 | os.makedirs(res, exist_ok=True) 33 | return res 34 | 35 | 36 | DEFAULT_WIKI_LOCATION = os.path.join(EXT_ROOT_DIRECTORY, 'wiki') 37 | 38 | def getWikiLocation(): 39 | res : str = shared.opts.data.get("readme_browser_wiki_location", "") 40 | if res == "": 41 | res = DEFAULT_WIKI_LOCATION 42 | os.makedirs(res, exist_ok=True) 43 | return res 44 | 45 | 46 | section = ("readme_browser", "Readme browser") 47 | options = { 48 | "readme_browser_tab_location": shared.OptionInfo( 49 | "Extensions -> Readme files", 50 | "Tab location", 51 | gr.Radio, 52 | { 53 | 'choices' : ["Extensions -> Readme files", "Readme files"], 54 | }, 55 | section=section, 56 | ).needs_reload_ui(), 57 | 58 | "readme_browser_hide_disabled_extensions": shared.OptionInfo( 59 | False, 60 | "Hide disabled extensions", 61 | gr.Checkbox, 62 | section=section, 63 | ).needs_reload_ui(), 64 | 65 | "readme_browser_need_cache": shared.OptionInfo( 66 | True, 67 | "Need cache some external-hosted media", 68 | gr.Checkbox, 69 | section=section, 70 | ).info('github user content and repo assets, imgur'), 71 | 72 | "readme_browser_need_cache_on_startup": shared.OptionInfo( 73 | False, 74 | "Cache these media and wikis for all extensions on the webui startup if last cache was made >= 72 hours ago", 75 | gr.Checkbox, 76 | section=section, 77 | ).needs_reload_ui(), 78 | 79 | "readme_browser_cache_location": shared.OptionInfo( 80 | "", 81 | "Cache location", 82 | gr.Textbox, 83 | { 84 | "placeholder": "Leave empty to use default 'sd-webui-readme-browser/cache' location", 85 | }, 86 | section=section, 87 | ).needs_reload_ui(), 88 | 89 | "readme_browser_wiki_location": shared.OptionInfo( 90 | "", 91 | "Cloned wiki location", 92 | gr.Textbox, 93 | { 94 | "placeholder": "Leave empty to use default 'sd-webui-readme-browser/wiki' location", 95 | }, 96 | section=section, 97 | ).needs_reload_ui(), 98 | } 99 | shared.options_templates.update(shared.options_section(section, options)) 100 | -------------------------------------------------------------------------------- /readme_browser/readme_files.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataclasses import dataclass 3 | from modules import extensions, util, paths_internal 4 | from readme_browser.options import needHideDisabledExtensions, getWikiLocation 5 | 6 | 7 | @dataclass 8 | class ReadmeFileData: 9 | filePath: str 10 | extPath: str 11 | 12 | 13 | readmeFilesByExtName: dict[str, ReadmeFileData] = {} 14 | 15 | def initReadmeFiles(): 16 | global readmeFilesByExtName 17 | readmeFilesByExtName = {} 18 | 19 | webuiPath = paths_internal.data_path 20 | webuiName = os.path.basename(webuiPath) 21 | files = util.listfiles(webuiPath) 22 | for file in files: 23 | if os.path.basename(file).lower() == 'readme.md': 24 | readmeFilesByExtName[webuiName] = ReadmeFileData(file, webuiPath) 25 | break 26 | 27 | for ext in extensions.extensions: 28 | if needHideDisabledExtensions() and not ext.enabled: continue 29 | files = util.listfiles(ext.path) 30 | for file in files: 31 | if os.path.basename(file).lower() == 'readme.md': 32 | readmeFilesByExtName[ext.name] = ReadmeFileData(file, ext.path) 33 | break 34 | 35 | for dir in os.listdir(getWikiLocation()): 36 | wikiPath = os.path.join(getWikiLocation(), dir) 37 | wikiName = os.path.basename(wikiPath) 38 | homeFile = os.path.join(wikiPath, 'Home.md') 39 | readmeFilesByExtName[f'wiki - {wikiName}'] = ReadmeFileData(homeFile, wikiPath) 40 | -------------------------------------------------------------------------------- /readme_browser/tools.py: -------------------------------------------------------------------------------- 1 | import re, datetime, os 2 | import urllib.parse 3 | from dataclasses import dataclass 4 | from pathlib import Path 5 | from modules.gitpython_hack import Repo 6 | from modules import errors, paths_internal 7 | from readme_browser.options import EXT_ROOT_DIRECTORY, getCacheLocation 8 | 9 | JS_PREFIX = 'readme_browser_javascript_' 10 | 11 | 12 | @dataclass 13 | class Anchor: 14 | name: str 15 | id: str 16 | depth: int 17 | 18 | 19 | def removeCodeBlocks(file: str) -> str: 20 | def replace(string, old, new): 21 | compiled = re.compile(old, re.MULTILINE) 22 | res = compiled.sub(new, string) 23 | return str(res) 24 | file = replace(file, r'(```[\s\S]*?```)', '') 25 | return file 26 | 27 | 28 | def makeFileIndex(file: str) -> str: 29 | file = removeCodeBlocks(file) 30 | anchors: list[Anchor] = [] 31 | anchorsIDs: dict[str, int] = {} 32 | hs: list[str] = re.findall(r'^#{1,6} +.+', file, re.MULTILINE) 33 | validChars = set('abcdefghijklmnopqrstuvwxyz0123456789-_') 34 | 35 | for h in hs: 36 | depth = len(h.split(' ')[0]) 37 | anchorName = h.lstrip('#').strip().removesuffix(':') 38 | anchor = anchorName.lower().replace(' ', '-') 39 | tmp = anchor 40 | for char in set(anchor): 41 | if char not in validChars: 42 | tmp = tmp.replace(char, '') 43 | anchor = tmp 44 | if anchor in anchorsIDs: 45 | num = anchorsIDs[anchor] 46 | anchorsIDs[anchor] += 1 47 | anchor += f'-{num}' 48 | else: 49 | anchorsIDs[anchor] = 1 50 | 51 | anchors.append(Anchor(anchorName, anchor, depth)) 52 | 53 | if len(anchors) <= 4: 54 | return "" 55 | 56 | minDepth = min(x.depth for x in anchors) 57 | for i in range(len(anchors)): 58 | anchors[i].depth = anchors[i].depth - minDepth 59 | 60 | result = '\n\n-----------------------\n\n
\nFile index\n\n' 61 | for anchor in anchors: 62 | filler = '' 63 | if anchor.depth > 0: 64 | filler = '    ' * anchor.depth 65 | result += f'{filler}[{anchor.name}](#{anchor.id})\\\n' 66 | result = result[:-2] 67 | result += '\n\n
\n\n' 68 | 69 | return result 70 | 71 | 72 | def addJumpAnchors(file: str) -> str: 73 | if file.count('\n') <= 30: 74 | topInvisible = '' 75 | return f'{topInvisible}\n\n{file}\n' 76 | 77 | top = 'Go to the bottom ↓' 78 | bottom = 'Go to the top ↑' 79 | 80 | return f'{top}\n\n{file}\n\n{bottom}\n' 81 | 82 | 83 | def getURLsFromFile(file: str) -> list[str]: 84 | urls = set() 85 | 86 | MDLinks = re.findall(r'\[.*?\]\((.+?)\)', file) 87 | for link in MDLinks: 88 | urls.add(link) 89 | 90 | srcLinks = re.findall(r'src="(.+?)"', file) 91 | for link in srcLinks: 92 | urls.add(link) 93 | 94 | hrefLinks = re.findall(r'href="(.+?)"', file) 95 | for link in hrefLinks: 96 | urls.add(link) 97 | 98 | httpsLinks = re.findall(r'(^|\s)(https://.+?)($|\s)', file, re.MULTILINE) 99 | for link in httpsLinks: 100 | link = link[1].removesuffix('.') 101 | urls.add(link) 102 | 103 | return urls 104 | 105 | 106 | def replaceURLInFile(file: str, oldUrl: str, newUrl: str) -> str: 107 | foundIdx = file.find(oldUrl) 108 | while foundIdx != -1: 109 | try: 110 | needReplaceLeft = False 111 | if file[foundIdx-len('href="'):foundIdx] == 'href="': 112 | needReplaceLeft = True 113 | elif file[foundIdx-len('src="'):foundIdx] == 'src="': 114 | needReplaceLeft = True 115 | elif file[foundIdx-len(']('):foundIdx] == '](': 116 | needReplaceLeft = True 117 | elif oldUrl.lower().startswith('https://'): 118 | needReplaceLeft = True 119 | newUrl = f'[{newUrl}]({newUrl})' 120 | 121 | needReplaceRight = False 122 | if file[foundIdx+len(oldUrl)] in ')]}>"\' \\\n.,': 123 | needReplaceRight = True 124 | 125 | if needReplaceLeft and needReplaceRight: 126 | file = file[0:foundIdx] + newUrl + file[foundIdx+len(oldUrl):] 127 | 128 | except IndexError: 129 | pass 130 | 131 | foundIdx = file.find(oldUrl, foundIdx+1) 132 | 133 | return file 134 | 135 | 136 | def isLocalURL(url: str): 137 | return not ('://' in url or url.startswith('//')) 138 | 139 | def isAnchor(url: str): 140 | return url.startswith('#') 141 | 142 | def isMarkdown(url: str): 143 | if '#' in url: 144 | url = url.removesuffix('#' + url.split('#')[-1]) 145 | return url.endswith('.md') 146 | 147 | 148 | def makeOpenRepoLink(extPath: str): 149 | url: str = None 150 | try: 151 | url = next(Repo(extPath).remote().urls, None) 152 | except Exception: 153 | pass 154 | if not url: 155 | return "" 156 | 157 | siteName = 'repository' 158 | if 'github.com' in url.lower(): 159 | if url.endswith('.wiki.git'): 160 | siteName = 'wiki on GitHub' 161 | url = url.removesuffix('.wiki.git') + '/wiki' 162 | else: 163 | siteName = 'GitHub page' 164 | 165 | return f"[Open {siteName}]({url})↗" 166 | 167 | 168 | def saveLastCacheDatetime(extName: str): 169 | file = os.path.join(getCacheLocation(), extName, 'lastCacheDatetime') 170 | os.makedirs(os.path.dirname(file), exist_ok=True) 171 | with open(file, 'w') as f: 172 | f.write(datetime.datetime.now().strftime("%d-%b-%Y (%H:%M:%S.%f)")) 173 | 174 | 175 | def readLastCacheDatetime(extName: str) -> datetime.datetime: 176 | dt = None 177 | file = os.path.join(getCacheLocation(), extName, 'lastCacheDatetime') 178 | if os.path.exists(file): 179 | with open(file, 'r') as f: 180 | dt = datetime.datetime.strptime(f.readline().removesuffix('\n'), "%d-%b-%Y (%H:%M:%S.%f)") 181 | return dt 182 | 183 | 184 | def enoughTimeLeftForCache(extName: str) -> bool: 185 | last = None 186 | try: 187 | last = readLastCacheDatetime(extName) 188 | except Exception as e: 189 | errors.report(f"Can't readLastCacheAllDatetime {e}", exc_info=True) 190 | 191 | return not last or datetime.datetime.now() - last >= datetime.timedelta(hours=72) 192 | 193 | 194 | 195 | def hasAllowedExt(url: str): 196 | url = url.lower() 197 | ALLOWED_EXTENSIONS = ['.jpeg', '.jpg', '.png', '.webp', '.gif', '.mp4', '.webm'] 198 | return any(url.endswith(x) for x in ALLOWED_EXTENSIONS) 199 | 200 | 201 | SPECIAL_EXTENSION_NAMES = [os.path.basename(EXT_ROOT_DIRECTORY), os.path.basename(paths_internal.data_path)] 202 | 203 | 204 | def makeAllMarkdownFilesList(extPath: str) -> str: 205 | if os.path.basename(extPath) in SPECIAL_EXTENSION_NAMES: 206 | return None 207 | 208 | allMarkdownFilesList = '' 209 | number = 0 210 | 211 | for filePath in Path(extPath).rglob('*.md'): 212 | fileName = os.path.basename(filePath) 213 | if not fileName.endswith('.md'): continue 214 | if fileName.startswith('_'): continue 215 | if fileName.startswith('.'): continue 216 | fullFileName = os.path.relpath(filePath, extPath) 217 | if fullFileName.startswith('.'): continue 218 | if fullFileName.startswith('venv'): continue 219 | allMarkdownFilesList += f"[{fullFileName}](/{urllib.parse.quote(fullFileName)})\n" 220 | number += 1 221 | 222 | if number <= 1: 223 | return None 224 | 225 | return allMarkdownFilesList 226 | -------------------------------------------------------------------------------- /readme_browser/wiki.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.parse 3 | from git import Repo 4 | from modules import util 5 | from readme_browser.options import getWikiLocation 6 | from readme_browser.tools import JS_PREFIX, enoughTimeLeftForCache 7 | 8 | 9 | def makeDummySidebar(dirPath: str) -> str: 10 | sidebar = '' 11 | for file in util.listfiles(dirPath): 12 | file = os.path.basename(file) 13 | if not file.endswith('.md'): continue 14 | if file.startswith('_'): continue 15 | name = file.removesuffix('.md') 16 | sidebar += f"[{name}](/{urllib.parse.quote(name)})\n" 17 | return sidebar 18 | 19 | 20 | def isWikiURL(url: str, extName: str): 21 | url = url.lower() 22 | 23 | if extName.startswith('wiki - '): 24 | tmp = extName.split(' - ') 25 | repoName = tmp[1] + '/' + tmp[2] 26 | isValidWiki = repoName in url 27 | else: 28 | isValidWiki = extName in url 29 | 30 | return 'github.com' in url and ('/wiki/' in url or url.endswith('/wiki')) and isValidWiki 31 | 32 | 33 | def getLocalWikiURL(url: str) -> str: 34 | url = url.lower() 35 | if url.endswith('/'): 36 | url = url[:-1] 37 | if '#' in url: 38 | url = url.removesuffix('#' + url.split('#')[-1]) 39 | if url.endswith('/wiki'): 40 | url += '/' 41 | tmp = url.split('/wiki/') 42 | repoURL = f'{tmp[0]}.wiki.git' 43 | wikiName = tmp[0].split('/')[-2] + " - " + tmp[0].split('/')[-1] 44 | filePath = tmp[1] 45 | 46 | dirPath = os.path.join(getWikiLocation(), wikiName) 47 | try: 48 | if not os.path.exists(dirPath): 49 | Repo.clone_from(repoURL, dirPath) 50 | elif enoughTimeLeftForCache(): 51 | repo = Repo(dirPath) 52 | repo.git.fetch(all=True) 53 | repo.git.reset('origin', hard=True) 54 | except Exception as e: 55 | # print(f"Cannot clone wiki {repoURL}:", e) 56 | pass 57 | 58 | link = f"{JS_PREFIX}readme_browser_openWiki('{wikiName}', '{filePath}')" 59 | return link 60 | 61 | 62 | def getWikiFilePath(wikiName, fileName): 63 | dirPath = os.path.join(getWikiLocation(), wikiName) 64 | if not fileName: 65 | fileName = 'Home.md' 66 | else: 67 | fileName += '.md' 68 | 69 | for file in util.listfiles(dirPath): 70 | file = os.path.basename(file) 71 | if file.lower() == fileName: 72 | fileName = file 73 | break 74 | 75 | return os.path.join(dirPath, fileName) 76 | 77 | -------------------------------------------------------------------------------- /scripts/readme_browser.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import gradio as gr 3 | from modules import script_callbacks, errors 4 | from readme_browser.main import getTabUI, cacheAll 5 | from readme_browser.options import needUseOnUICallback, needCacheOnStartup 6 | 7 | 8 | def onUITabs(): 9 | tab = getTabUI() 10 | return [(tab, "Readme files", "readme_files")] 11 | 12 | 13 | def addTabInExtensionsTab(component, **kwargs): 14 | try: 15 | if kwargs.get('elem_id', "") != 'extensions_backup_top_row': 16 | return 17 | tabs = component.parent.parent 18 | 19 | with tabs: 20 | with gr.Tab("Readme files", elem_id="readme_files_tab"): 21 | getTabUI() 22 | 23 | except Exception as e: 24 | errors.report(f"Can't add Readme files tab", exc_info=True) 25 | 26 | 27 | if needUseOnUICallback(): 28 | script_callbacks.on_ui_tabs(onUITabs) 29 | else: 30 | script_callbacks.on_after_component(addTabInExtensionsTab) 31 | 32 | 33 | if needCacheOnStartup(): 34 | script_callbacks.on_app_started(cacheAll) 35 | 36 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .readme-browser-file 2 | { 3 | max-width: 1012px; 4 | margin-right: auto !important; 5 | margin-left: auto !important; 6 | font-size: 16px !important; 7 | line-height: 1.1 !important; 8 | overflow-wrap: break-word; 9 | } 10 | 11 | .readme-browser-file table 12 | { 13 | display: block; 14 | overflow-x: auto; 15 | } 16 | 17 | .readme-browser-file strong 18 | { 19 | font-size: 105% !important; 20 | } 21 | 22 | .readme-browser-file a[href] 23 | { 24 | text-decoration: underline dotted !important; 25 | } 26 | 27 | .readme-browser-file a[href]:where([href="#readme_browser_top_anchor"])::after 28 | { 29 | content: '↪'; 30 | font-size: 60% !important; 31 | font-family: monospace; 32 | } 33 | 34 | .readme-browser-file a[href]:where([href^="#"]:not([href="#readme_browser_top_anchor"]))::after 35 | { 36 | content: '#'; 37 | font-size: 65% !important; 38 | font-family: monospace; 39 | } 40 | 41 | .readme-browser-file p, .readme-browser-file ol, .readme-browser-file ul 42 | { 43 | margin-bottom: 22px !important; 44 | } 45 | 46 | .readme-browser-file details 47 | { 48 | margin-bottom: 8px !important; 49 | } 50 | 51 | .readme-browser-file a[href]:not([href*="://"]):not([href^="#"])::before 52 | { 53 | content: '📎'; 54 | font-size: 88% !important; 55 | } 56 | --------------------------------------------------------------------------------