├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------