diff --git a/javascript/markdown_browser.js b/javascript/markdown_browser.js new file mode 100644 index 0000000..79c7f38 --- /dev/null +++ b/javascript/markdown_browser.js @@ -0,0 +1,67 @@ + +var markdown_browser_subFilePath = undefined; + +function markdown_browser_openSubFile_(dummy, extPath, extName) { + return [markdown_browser_subFilePath, extPath, extName]; +} + + +function markdown_browser_openSubFile(filePath) { + markdown_browser_subFilePath = decodeURI(filePath); + let button = gradioApp().getElementById('markdown_browser_openSubFileButton'); + button.click(); +} + + + + +function markdown_browser_alreadyHasAnchor(h) { + let elem = h.previousSibling; + if (!elem?.classList?.contains('markdown_browser_h_anchor')) return false; + return true; +} + + +function markdown_browser_afterRender() { + let file = gradioApp().getElementById('markdown_browser_file'); + let hElements = [...file.querySelectorAll("h1, h2, h3, h4, h5, h6")]; + let anchorNumbers = {}; + hElements.forEach((h) => { + if (markdown_browser_alreadyHasAnchor(h)) return; + if (!h.innerHTML) return; + let anchor = document.createElement('a'); + let anchorID = h.innerText.toLowerCase().replaceAll(' ', '-').replace(/[^a-zA-Z0-9-_]/g, ''); + if (anchorID in anchorNumbers) { + let key = anchorID; + anchorID += '-' + anchorNumbers[key]; + anchorNumbers[key] += 1; + } else { + anchorNumbers[anchorID] = 1; + } + anchor.setAttribute('id', anchorID); + anchor.classList.add('markdown_browser_h_anchor'); + h.parentNode.insertBefore(anchor, h); + }); + + + let aElements = [...file.getElementsByTagName('a')]; + aElements.forEach((a) => { + if (!a.href) return; + const url = new URL(a.href); + if (url.origin === window.location.origin && a.href.indexOf('#') !== -1) { + a.setAttribute('target', ''); + return; + } + const prefix = 'markdown_browser_javascript_'; + const prefixIndex = a.href.indexOf(prefix); + if (prefixIndex !== -1) { + let onClick = a.href.slice(prefixIndex + prefix.length); + onClick = decodeURI(onClick).replaceAll("%2C", ","); + a.setAttribute('onclick', onClick); + a.setAttribute('target', ''); + a.href = '#markdown_browser_top_anchor'; + } + }); +} + + diff --git a/replacer/other/markdown_browser.py b/replacer/other/markdown_browser.py new file mode 100644 index 0000000..8bf9448 --- /dev/null +++ b/replacer/other/markdown_browser.py @@ -0,0 +1,95 @@ +import os +import urllib.parse +import gradio as gr +from replacer.other.markdown_browser_tools import ( getURLsFromFile, JS_PREFIX, isLocalURL, isAnchor, isMarkdown, + replaceURLInFile, getAllDocuments, +) +from replacer.options import EXT_ROOT_DIRECTORY + + +def renderMarkdownFile(filePath: str, extDir: str): + with open(filePath, mode='r', encoding="utf-8-sig") as f: + file = f.read() + + for url in getURLsFromFile(file): + originalURL = url + replacementUrl = None + if JS_PREFIX in originalURL: + file = file.replace(originalURL, "***") + continue + + if isLocalURL(url): + if isAnchor(url): continue + if '#' in url: + url = url.removesuffix('#' + url.split('#')[-1]) + + if url[0] == '/': + urlFullPath = os.path.join(extDir, url[1:]) + else: + urlFullPath = os.path.join(os.path.dirname(filePath), url) + + if os.path.exists(urlFullPath): + if isMarkdown(url): + replacementUrl = f"{JS_PREFIX}markdown_browser_openSubFile('{urlFullPath}')" + else: + replacementUrl = f'file={urlFullPath}' + + if replacementUrl is not None: + replacementUrl = urllib.parse.quote(replacementUrl) + file = replaceURLInFile(file, originalURL, replacementUrl) + + return file + + +def openSubFile(filePath: str): + file = renderMarkdownFile(filePath, EXT_ROOT_DIRECTORY) + return file + +def openDocument(docName: str): + if not docName: return "" + file = renderMarkdownFile(g_documents[docName], EXT_ROOT_DIRECTORY) + return file + + +markdownFile = gr.Markdown("", elem_classes=['markdown-browser-file'], elem_id='markdown_browser_file') +g_documents = None + +def getDocsTabUI(): + global g_documents + g_documents = getAllDocuments() + + with gr.Blocks() as tab: + dummy_component = gr.Textbox("", visible=False) + + with gr.Row(): + selectedDocument = gr.Dropdown( + label="Document", + value="", + choices=[""] + list(g_documents.keys()) + ) + selectButton = gr.Button('Select') + selectButton.click( + fn=openDocument, + inputs=[selectedDocument], + outputs=[markdownFile], + ).then( + fn=None, + _js='markdown_browser_afterRender', + ) + + with gr.Row(): + markdownFile.render() + + openSubFileButton = gr.Button("", visible=False, elem_id="markdown_browser_openSubFileButton") + openSubFileButton.click( + fn=openSubFile, + _js="markdown_browser_openSubFile_", + inputs=[dummy_component], + outputs=[markdownFile] + ).then( + fn=None, + _js='markdown_browser_afterRender', + ) + + return tab + diff --git a/replacer/other/markdown_browser_tools.py b/replacer/other/markdown_browser_tools.py new file mode 100644 index 0000000..a984cfb --- /dev/null +++ b/replacer/other/markdown_browser_tools.py @@ -0,0 +1,89 @@ +import re, os +from dataclasses import dataclass +from replacer.options import EXT_ROOT_DIRECTORY +from modules import shared + +JS_PREFIX = 'markdown_browser_javascript_' + + +@dataclass +class Anchor: + name: str + id: str + depth: int + + +def getURLsFromFile(file: str) -> list[str]: + urls = set() + + MDLinks = re.findall(r'\[.*?\]\((.+?)\)', file) + for link in MDLinks: + urls.add(link) + + srcLinks = re.findall(r'src="(.+?)"', file) + for link in srcLinks: + urls.add(link) + + hrefLinks = re.findall(r'href="(.+?)"', file) + for link in hrefLinks: + urls.add(link) + + httpsLinks = re.findall(r'(^|\s)(https://.+?)($|\s)', file, re.MULTILINE) + for link in httpsLinks: + link = link[1].removesuffix('.') + urls.add(link) + + return urls + + +def replaceURLInFile(file: str, oldUrl: str, newUrl: str) -> str: + foundIdx = file.find(oldUrl) + while foundIdx != -1: + try: + needReplaceLeft = False + if file[foundIdx-len('href="'):foundIdx] == 'href="': + needReplaceLeft = True + elif file[foundIdx-len('src="'):foundIdx] == 'src="': + needReplaceLeft = True + elif file[foundIdx-len(']('):foundIdx] == '](': + needReplaceLeft = True + elif oldUrl.lower().startswith('https://'): + needReplaceLeft = True + newUrl = f'[{newUrl}]({newUrl})' + + needReplaceRight = False + if file[foundIdx+len(oldUrl)] in ')]}>"\' \\\n.,': + needReplaceRight = True + + if needReplaceLeft and needReplaceRight: + file = file[0:foundIdx] + newUrl + file[foundIdx+len(oldUrl):] + + except IndexError: + pass + + foundIdx = file.find(oldUrl, foundIdx+1) + + return file + + +def isLocalURL(url: str): + return not ('://' in url or url.startswith('//')) + +def isAnchor(url: str): + return url.startswith('#') + +def isMarkdown(url: str): + if '#' in url: + url = url.removesuffix('#' + url.split('#')[-1]) + return url.endswith('.md') + + +def getAllDocuments() -> dict[str, str]: + docs = dict() + files = shared.listfiles(os.path.join(EXT_ROOT_DIRECTORY, 'docs')) + for file in files: + if not file.endswith(".md"): continue + fileName = os.path.basename(file).removesuffix(".md").capitalize() + docs[fileName] = file + docs["Readme"] = os.path.join(EXT_ROOT_DIRECTORY, 'README.md') + return docs diff --git a/scripts/replacer_main_ui.py b/scripts/replacer_main_ui.py index 3fa8f13..5258fa5 100644 --- a/scripts/replacer_main_ui.py +++ b/scripts/replacer_main_ui.py @@ -9,6 +9,7 @@ from replacer.tools import getReplacerFooter from replacer.ui.tools_ui import watchOutputPanel, watchSetCustomScriptSourceForComponents from replacer.extensions import replacer_extensions +from replacer.other.markdown_browser import getDocsTabUI @@ -50,6 +51,8 @@ def mountDedicatedPage(demo, app): with gr.Blocks(analytics_enabled=False) as extras_interface: ui_postprocessing.create_ui() replacer_extensions.image_comparison.mountImageComparisonTab() + with gr.Tab(label="Docs", elem_id=f"tab_docs"): + tab_docs = getDocsTabUI() footer = getReplacerFooter() gr.HTML(footer, elem_id="footer") diff --git a/style.css b/style.css index a2421b6..36ed48a 100644 --- a/style.css +++ b/style.css @@ -36,3 +36,63 @@ button.replacer-pause-button{ #replacer_video_gallery { height: 440px; } + + + + + +.markdown-browser-file +{ + max-width: 1012px; + margin-right: auto !important; + margin-left: auto !important; + font-size: 16px !important; + line-height: 1.1 !important; + overflow-wrap: break-word; +} + +.markdown-browser-file table +{ + display: block; + overflow-x: auto; +} + +.markdown-browser-file strong +{ + font-size: 105% !important; +} + +.markdown-browser-file a[href] +{ + text-decoration: underline dotted !important; +} + +.markdown-browser-file a[href]:where([href="#markdown_browser_top_anchor"])::after +{ + content: '↪'; + font-size: 60% !important; + font-family: monospace; +} + +.markdown-browser-file a[href]:where([href^="#"]:not([href="#markdown_browser_top_anchor"]))::after +{ + content: '#'; + font-size: 65% !important; + font-family: monospace; +} + +.markdown-browser-file p, .markdown-browser-file ol, .markdown-browser-file ul +{ + margin-bottom: 22px !important; +} + +.markdown-browser-file details +{ + margin-bottom: 8px !important; +} + +.markdown-browser-file a[href]:not([href*="://"]):not([href^="#"])::before +{ + content: '📎'; + font-size: 88% !important; +}