From 68baa89e1dc370afb863eec1f0dc7c657befcfdc Mon Sep 17 00:00:00 2001 From: Vincent Nguyen Date: Fri, 10 Jan 2025 23:51:04 +0100 Subject: [PATCH 01/14] [Navigate Results Modern][Added] Revamped version of the current output --- kibot/out_navigate_results_modern.py | 2016 ++++++++++++++++++++++++++ 1 file changed, 2016 insertions(+) create mode 100644 kibot/out_navigate_results_modern.py diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py new file mode 100644 index 000000000..b386750f8 --- /dev/null +++ b/kibot/out_navigate_results_modern.py @@ -0,0 +1,2016 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022-2025 Salvador E. Tropea +# Copyright (c) 2022-2025 Nguyen Vincent +# Copyright (c) 2022-2025 Instituto Nacional de Tecnología Industrial +# Contributed by Nguyen Vincent (@nguyen-v) +# License: AGPL-3.0 +# Project: KiBot (formerly KiPlot) +# The Assembly image is a composition from Pixlok and oNline Web Fonts +# The rest are KiCad icons +""" +Dependencies: + - from: RSVG + role: Create outputs preview + id: rsvg1 + - from: RSVG + role: Create PNG icons + id: rsvg2 + - from: Ghostscript + role: Create outputs preview + - from: ImageMagick + role: Create outputs preview + - from: Git + role: Find origin url +""" +import base64 +import os +import re +import subprocess +import pprint +from shutil import copy2 +from math import ceil +from .bom.kibot_logo import KIBOT_LOGO +from .error import KiPlotConfigurationError +from .gs import GS +from .optionable import Optionable, BaseOptions +from .kiplot import config_output, get_output_dir, run_command +from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS, read_png, force_list +from .pre_base import BasePreFlight +from .registrable import RegOutput +from .macros import macros, document, output_class # noqa: F401 +from . import log, __version__ + +logger = log.get_logger() +EXT_IMAGE = {'gbr': 'file_gbr', + 'gtl': 'file_gbr', + 'gtp': 'file_gbr', + 'gbo': 'file_gbr', + 'gto': 'file_gbr', + 'gbs': 'file_gbr', + 'gbl': 'file_gbr', + 'gts': 'file_gbr', + 'gml': 'file_gbr', + 'gm1': 'file_gbr', + 'gbrjob': 'file_gerber_job', + 'brd': 'file_brd', + 'bz2': 'file_bz2', + 'dxf': 'file_dxf', + 'cad': 'file_cad', + 'drl': 'file_drl', + 'pdf': 'file_pdf', + 'txt': 'file_txt', + 'pos': 'file_pos', + 'csv': 'file_csv', + 'svg': 'file_svg', + 'eps': 'file_eps', + 'png': 'file_png', + 'jpg': 'file_jpg', + 'plt': 'file_plt', + 'ps': 'file_ps', + 'rar': 'file_rar', + 'scad': 'file_scad', + 'stl': 'file_stl', + 'step': 'file_stp', + 'stp': 'file_stp', + 'wrl': 'file_wrl', + 'html': 'file_html', + 'css': 'file_css', + 'xml': 'file_xml', + 'tsv': 'file_tsv', + 'xlsx': 'file_xlsx', + 'xyrs': 'file_xyrs', + 'xz': 'file_xz', + 'gz': 'file_gz', + 'tar': 'file_tar', + 'zip': 'file_zip', + 'kicad_pcb': 'pcbnew', + 'sch': 'eeschema', + 'kicad_sch': 'eeschema', + 'blend': 'file_blend', + 'pcb3d': 'file_pcb3d', + 'json': 'file_json'} +for i in range(31): + n = str(i) + EXT_IMAGE['gl'+n] = 'file_gbr' + EXT_IMAGE['g'+n] = 'file_gbr' + EXT_IMAGE['gp'+n] = 'file_gbr' + +BIG_ICON = 512 +MID_ICON = 64 + +IMAGEABLES_SIMPLE = {'png', 'jpg'} +IMAGEABLES_GS = {'pdf', 'eps', 'ps'} +IMAGEABLES_SVG = {'svg'} +TITLE_HEIGHT = 30 +STYLE = """ + +/* Colors =================================================================== */ + +:root { + --light-bg-color: #ffffff; + --dark-bg-color: #1e1e2f; + --light-bg-color-banner: #dfdfdf; + --dark-bg-color-banner: #27293d; + --light-text-color: #444444; + --dark-text-color: #e5e5e5; + --light-hover-color: #902ec9; + --light-hover-color-act: #652f85; + --dark-hover-color: #ffa500; + --dark-hover-color-act: #cc8400; + --dark-text-color-accent: #a3a3c2; + --light-text-color-accent: #444444; + --light-banner-hover: #b0b0b0; + --dark-banner-hover: #383b4b; + --text-color-accent: #a3a3c2; +} + +/* Main body ================================================================ */ + +body { + margin: 0; + font-family: 'Roboto', sans-serif; + background-color: var(--dark-bg-color); + color: var(--dark-text-color); + transition: + background-color 0.4s ease, + color 0.4s ease, + transition: scrollbar-color 0.2s ease-in-out; +} + +body.dark-mode { + --text-color-accent: var(--dark-text-color-accent); + background-color: var(--dark-bg-color); + color: var(--dark-text-color); +} + +body.light-mode { + --text-color-accent: var(--light-text-color-accent); + background-color: var(--light-bg-color); + color: var(--light-text-color); +} + +/* Top Menu ================================================================= */ + +/* Layout is as follows */ +/* [X/☰] [↩] [↪] (Logo) [☾/☀] [🏠︎] */ + +#topmenu { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 1000; + background-color: var(--dark-bg-color-banner); + padding: 10px 0; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: space-between; + transition: background-color 0.2s ease, color 0.2s ease; +} + +body.light-mode #topmenu { + background-color: var(--light-bg-color-banner); +} + +body.dark-mode #topmenu { + background-color: var(--dark-bg-color-banner); +} + +/* Buttons ================================================================== */ + +/* button corresponds to the navigation buttons (forward, backward, home) */ + +button, #open-sidenav, #close-sidenav { + background: none; + border: none; + color: var(--dark-text-color); + cursor: pointer; + transition: color 0.3s ease; +} + +body.light-mode #topmenu button, +body.light-mode #topmenu #open-sidenav, +body.light-mode #topmenu #close-sidenav { + color: var(--light-text-color); +} + +body.dark-mode #topmenu button, +body.dark-mode #topmenu #open-sidenav, +body.dark-mode #topmenu #close-sidenav { + color: var(--dark-text-color); +} + +button { + font-size: 20px; + margin: 0 10px; +} + +#open-sidenav, #close-sidenav { + width: 36px; + height: 36px; + line-height: 36px; + text-align: center; + font-size: 28px; + margin-left: 10px; + user-select: none; /* Prevent text selection */ +} + +/* Hover effects */ + +button:hover, #open-sidenav:hover, #close-sidenav:hover { + color: var(--dark-hover-color); +} + +body.dark-mode #topmenu button:hover, +body.dark-mode #topmenu #open-sidenav:hover, +body.dark-mode #topmenu #close-sidenav:hover { + color: var(--dark-hover-color); +} + +body.light-mode #topmenu button:hover, +body.light-mode #topmenu #open-sidenav:hover, +body.light-mode #topmenu #close-sidenav:hover { + color: var(--light-hover-color); +} + +/* Active effects */ + +button:active, #open-sidenav:active, #close-sidenav:active { + color: var(--dark-hover-color-act); + transition: none; +} + +body.dark-mode #topmenu button:active, +body.dark-mode #topmenu #open-sidenav:active, +body.dark-mode #topmenu #close-sidenav:active { + color: var(--dark-hover-color-act); +} + +body.light-mode #topmenu button:active, +body.light-mode #topmenu #open-sidenav:active, +body.light-mode #topmenu #close-sidenav:active { + color: var(--light-hover-color-act); +} + +/* Sidebar Navigation ======================================================= */ + +.sidenav { + position: fixed; + width: 0; /* Initially collapsed */ + height: calc(100% - var(--top-menu-height, 60px)); + top: var(--top-menu-height, 60px); + left: 0; + background-color: #27293d; + overflow-x: hidden; + overflow-y: auto; + transition: + width 0.5s ease, + padding-left 0.5s ease, + scrollbar-color 0.2s ease-in-out, + background-color 0.2s ease-in-out; + box-sizing: border-box; + padding-top: 0; +} + + +body.dark-mode .sidenav { + background-color: var(--dark-bg-color-banner); +} + +body.light-mode .sidenav { + background-color: var(--light-bg-color-banner); +} + +.sidenav > ul:first-child { + margin-top: 20px; /* Padding between top menu and first element of sidenav */ +} + +/* Side Navigation Outputs -------------------------------------------------- */ + +.sidenav-output { + padding: 8px 30px; + text-decoration: none; + font-size: 16px; + color: var(--dark-text-color); + display: block; + transition: color 0.3s ease; + border-radius: 4px; +} + +body.light-mode .sidenav-output { + color: var(--light-text-color); +} + +body.dark-mode .sidenav-output { + color: var(--dark-text-color); +} + +/* Hover effects */ + +.sidenav-output:hover { + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); +} + +body.dark-mode .sidenav-output:hover { + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); +} + +body.light-mode .sidenav-output:hover { + color: var(--light-hover-color); + background-color: var(--light-banner-hover); +} + +/* Active effects */ + +.sidenav-output:active { + color: var(--dark-hover-color-act); +} + +body.dark-mode .sidenav-output:active { + color: var(--dark-hover-color-act); +} + +body.light-mode .sidenav-output:active { + color: var(--light-hover-color-act); +} + +/* Side Navigation Categories ----------------------------------------------- */ + +.sidenav-category { + list-style: none; + padding: 0; + margin: 0; + user-select: none; /* Prevent text selection */ +} + +.sidenav-category .folder > span { + display: flex; + align-items: center; + cursor: pointer; + color: var(--dark-text-color-accent); + padding: 10px 20px; + margin-bottom: 0px; + width: 100%; + transition: background-color 0.3s, color 0.3s; + border-radius: 4px; +} + +.sidenav-category .folder-contents { + list-style: none; + margin-left: 20px; + padding: 0; +} + +body.dark-mode .sidenav-category .folder > span { + color: var(--dark-text-color-accent); +} + +body.light-mode .sidenav-category .folder > span { + color: var(--light-text-color); +} + +/* Hover effects */ + +.sidenav-category .folder > span:hover { + background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); +} + +body.dark-mode .sidenav-category .folder > span:hover { + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); +} + +body.light-mode .sidenav-category .folder > span:hover { + color: var(--light-hover-color); + background-color: var(--light-banner-hover); +} + +/* Active effects */ + +.sidenav-category .folder > span:active { + color: var(--dark-hover-color); + transition: none; +} + +body.dark-mode .sidenav-category .folder > span:active { + color: var(--dark-hover-color-act); +} + +body.light-mode .sidenav-category .folder > span:active { + color: var(--light-hover-color-act); +} + +/* Chevron (arrow) styling -------------------------------------------------- */ + +.chevron { + display: block; + width: 0; + height: 0; + border: 8px solid transparent; + border-left-color: #606077; + margin-right: 8px; + transform-origin: 25% 50%; + transition: transform 0.3s ease, border-left-color 0.3s ease; + pointer-events: none; +} + +body.dark-mode .chevron { + border-left-color: #606077; +} + +body.light-mode .chevron { + border-left-color: #909090; +} + +/* We change styles for when the chevron is pointing down */ + +.folder.open > span .chevron { + border-left-color: var(--dark-text-color-accent); + transform: rotate(90deg); +} + +body.dark-mode .folder.open > span .chevron { + border-left-color: var(--dark-text-color-accent); +} + +body.light-mode .folder.open > span .chevron { + border-left-color: var(--light-text-color-accent); +} + +/* Hover effects */ + +body.dark-mode .folder > span:hover .chevron { + border-left-color: var(--dark-hover-color) +} + +body.light-mode .folder > span:hover .chevron { + border-left-color: var(--light-hover-color) +} + +/* Active effects */ + +body.dark-mode .folder > span:active .chevron { + border-left-color: var(--dark-hover-color-act) +} + + +body.light-mode .folder > span:active .chevron { + border-left-color: var(--light-hover-color-act) +} + +/* Main content ============================================================= */ + +#main { + transition: margin-left 0.5s; + padding: 16px; + margin-top: 80px; +} + +/* Comment field of output is used as a title for each output */ + +.output-comment { + font-size: 1.4em; + font-weight: 500; + color: var(--dark-text-color); + margin: 20px 0 10px 0; + text-align: center; +} + +body.light-mode .output-comment { + color: var(--light-text-color); +} + +body.dark-mode .output-comment { + color: var(--dark-text-color); +} + +/* Category boxes (folder) -------------------------------------------------- */ + +.category-box { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--dark-bg-color-banner); + border: 1px solid var(--dark-bg-color-banner); + border-radius: 8px; + padding: 10px 20px; + text-decoration: none; + color: var(--light-text-color); + max-width: 400px; + transition: background-color 0.3s ease, transform 0.2s ease; + margin: 10px auto; +} + +body.light-mode .category-box { + color: var(--light-text-color); + background-color: var(--light-bg-color-banner); + border: var(--light-bg-color-banner); +} + + +body.dark-mode .category-box { + color: var(--dark-text-color); + background-color: var(--dark-bg-color-banner); + border: var(--dark-bg-color-banner); +} + +.category-title { + font-size: 1.4em; + font-weight: 500; + text-align: center; + color: #e5e5e5; + text-decoration: none; + display: inline-block; +} + +body.light-mode .category-title { + color: var(--light-text-color); +} + +body.dark-mode .category-title { + color: var(--dark-text-color); +} + +/* Hover effects */ + +.category-box:hover { + background-color: var(--dark-banner-hover); + transform: scale(1.05); /* Slight zoom effect */ + cursor: pointer; +} + +body.light-mode .category-box:hover { + background-color: var(--light-banner-hover); +} + +body.dark-mode .category-box:hover { + background-color: var(--dark-banner-hover); +} + +/* Output boxes (files) ----------------------------------------------------- */ + +.output-box { + background-color: var(--dark-bg-color-banner); + border: 1px solid var(--dark-bg-color-banner); + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 300px; + height: 140px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +/* Offset the scroll position */ +.output-virtual-box { + position: relative; + padding-top: var(--top-menu-height, 80px); + margin-top: calc(-1 * var(--top-menu-height, 80px)); +} + +/* Some files (e.g. PDF, PNG) have wider output boxes */ + +.output-box.wide { + width: 400px; + height: auto; +} + +.output-box img { + max-width: 100%; + max-height: 100%; + height: auto; + margin-bottom: 10px; +} + +/* The output boxes are centered and wrap around */ + +.items-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + padding: 20px; +} + +body.light-mode .output-box { + color: var(--light-text-color); + background-color: var(--light-bg-color-banner); + border: var(--light-bg-color-banner); +} + +body.dark-mode .output-box { + color: var(--dark-text-color); + background-color: var(--dark-bg-color-banner); + border: var(--dark-bg-color-banner); +} + +/* Hover effects */ + +.output-box:hover { + background-color: var(--dark-banner-hover); + transform: scale(1.05); + cursor: pointer; +} + +body.light-mode .output-box:hover { + background-color: var(--light-banner-hover); +} + +body.dark-mode .output-box:hover { + background-color: var(--dark-banner-hover); +} + +/* Name of the output below the icon */ + +.output-box .output-name { + color: #8997c6; + font-size: 14px; + margin-top: 8px; + text-align: center; +} + +body.light-mode .output-box .output-name { + color: var(--light-text-color-accent); +} + +body.dark-mode .output-box .output-name { + color: #8997c6; +} + +/* Filename below the icon */ + +.output-box .filename { + text-decoration: none; + color: var(--dark-text-color); + text-align: center; + font-size: 14px; +} + +body.light-mode .output-box .filename { + color: var(--light-text-color); +} + +body.dark-mode .output-box .filename { + color: var(--dark-text-color); +} + +/* Theme Toggle Switch ====================================================== */ + +.theme-switch { + position: relative; + display: inline-block; + width: 50px; + height: 25px; + margin-left: 10px; +} + +/* Hide the default checkbox button */ + +.theme-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.theme-switch span { + position: absolute; + cursor: pointer; + background-color: var(--light-banner-hover); + border-radius: 25px; + top: 0; + left: 0; + right: 0; + bottom: 0; + transform: translateY(-30%); /* Center vertically */ + transition: 0.4s; +} + +.theme-switch span::before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 3px; + background-color: var(--light-bg-color); + border-radius: 50%; + transition: none; /* Disable animation by default */ +} + +.theme-switch span.animate::before { + transition: transform 0.4s ease, background-color 0.4s ease; +} + +.theme-switch input:checked + span { + background-color: var(--dark-bg-color); +} + +.theme-switch input:checked + span::before { + transform: translateX(25px); + background-color: var(--dark-text-color); +} + +/* Scrollbar ================================================================ */ + +body, html { + scroll-behavior: smooth; + scrollbar-width: auto; +} + +body.dark-mode .sidenav { + scrollbar-color: var(--dark-banner-hover) var(--dark-bg-color); +} + +body.light-mode .sidenav { + scrollbar-color: var(--light-banner-hover) var(--light-bg-color); +} + +/* WebKit Scrollbar Styles */ +body::-webkit-scrollbar, .sidenav::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +body::-webkit-scrollbar-thumb, .sidenav::-webkit-scrollbar-thumb { + border-radius: 6px; + background: var(--dark-banner-hover); + border: 2px solid var(--dark-bg-color); +} + +body::-webkit-scrollbar-track, .sidenav::-webkit-scrollbar-track { + border-radius: 6px; + background: var(--dark-bg-color); +} + +body.dark-mode::-webkit-scrollbar-thumb:hover, .sidenav.dark-mode::-webkit-scrollbar-thumb:hover { + background: #44475a !important; +} + +body.light-mode::-webkit-scrollbar-thumb, .sidenav.light-mode::-webkit-scrollbar-thumb { + background: var(--light-banner-hover); + border: 2px solid var(--light-bg-color); +} + +body.light-mode::-webkit-scrollbar-track, .sidenav.light-mode::-webkit-scrollbar-track { + background: var(--light-bg-color); +} + +body.light-mode::-webkit-scrollbar-thumb:hover, .sidenav.light-mode::-webkit-scrollbar-thumb:hover { + background: #909090 !important; +} + +body::-webkit-scrollbar-corner, .sidenav::-webkit-scrollbar-corner { + background: var(--dark-bg-color); +} + +/* Markdown ================================================================= */ + +.markdown-content { + font-family: Roboto, sans-serif; + line-height: 1.6; + margin-left: 150px; + margin-right: 150px; + padding: 15px; + border-radius: 5px; + white-space: pre-wrap; /* Handle preformatted text */ + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.light-mode .markdown-content { + background-color: #f9f9f9; + border: 1px solid #ddd; + color: #444444; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.dark-mode .markdown-content { + background-color: #1e1e2f; + border: 1px solid #44475a; + color: #e5e5e5; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +/* Tables */ +.markdown-content table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.light-mode .markdown-content table th, +body.light-mode .markdown-content table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + background-color: #ffffff; + color: #444444; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.dark-mode .markdown-content table th, +body.dark-mode .markdown-content table td { + border: 1px solid #44475a; + padding: 8px; + text-align: left; + background-color: #27293d; + color: #e5e5e5; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +/* Code Blocks */ +.markdown-content pre { + background-color: var(--dark-bg-color-banner); + color: var(--dark-text-color); /* Matches dark theme text */ + padding: 10px; + border-radius: 5px; + overflow-x: auto; + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.light-mode .markdown-content pre { + background-color: var(--light-bg-color-banner); + color: var(--light-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.dark-mode .markdown-content pre { + background-color: var(--dark-bg-color-banner); + color: var(--dark-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +/* Inline Code */ +.markdown-content code { + background-color: var(--light-bg-color-banner); + padding: 2px 5px; + border-radius: 3px; + font-family: 'Courier New', Courier, monospace; + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.light-mode .markdown-content code { + background-color: var(--light-bg-color-banner); + color: var(--light-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.dark-mode .markdown-content code { + background-color: var(--dark-bg-color-banner); + color: var(--dark-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +/* Links */ +body.light-mode .markdown-content a { + color: var(--light-hover-color); + text-decoration: none; + transition: color 0.4s ease; +} + +body.dark-mode .markdown-content a { + color: var(--dark-hover-color); + text-decoration: none; + transition: color 0.4s ease; +} + +.markdown-content a:hover { + text-decoration: underline; +} + +/* Images */ +.markdown-content img { + max-width: 100%; + height: auto; + display: block; + margin: 10px auto; + transition: opacity 0.4s ease; +} + +/* Search bar =============================================================== */ + +#search-container, +#search-bar, +#autocomplete-list, +#autocomplete-list li { + transition: background-color 0.3s, color 0.3s, border-color 0.3s; +} + +#search-container { + padding: 10px; + background-color: transparent; + margin-top: 10px; + top: 0; + z-index: 1001; + width: calc(100% - 10px); + box-sizing: border-box; +} + +#search-bar { + width: 100%; /* Match the width of the container */ + padding: 8px; + border: 1px solid var(--light-text-color-accent); + border-radius: 4px; + outline: none; + background-color: transparent; + color: var(--light-text-color); + box-sizing: border-box; /* Ensure padding is included in width */ +} + +#search-bar::placeholder { + color: var(--light-text-color-accent); +} + +#autocomplete-list { + list-style-type: none; + padding: 0; + margin: 5px 0 0; + max-height: 200px; + overflow-y: auto; + background-color: var(--light-bg-color-banner); + border: 1px solid var(--light-text-color-accent); + border-radius: 4px; + position: absolute; + z-index: 1001; + width: auto; /* Width will be dynamically calculated */ + box-sizing: border-box; + display: none; /* Hidden by default */ +} + +#autocomplete-list li { + padding: 8px; + cursor: pointer; + transition: background-color 0.2s; + color: var(--light-text-color); +} + +#autocomplete-list li:hover { + background-color: var(--light-banner-hover); + color: var(--light-hover-color); +} + +.dark-mode #search-bar { + color: var(--dark-text-color); + border-color: var(--dark-text-color-accent); +} + +.dark-mode #search-bar::placeholder { + color: var(--dark-text-color-accent); +} + +.dark-mode #autocomplete-list { + background-color: var(--dark-bg-color-banner); + border-color: var(--dark-text-color-accent); +} + +.dark-mode #autocomplete-list li { + color: var(--dark-text-color); +} + +.dark-mode #autocomplete-list li:hover { + background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); +} + + +/* New classes to remove transitions on page load =========================== */ + +body.no-transition, +.no-transition .output-box, +body.no-transition .theme-switch span, +body.no-transition button, +body.no-transition #close-sidenav, +body.no-transition #home-button, +body.no-transition #back-button, +body.no-transition #forward-button, +body.no-transition #topmenu, +body.no-transition .sidenav-category .folder > span, +body.no-transition .category-box { + transition: none !important; /* Disable transition during page load */ +} + +""" +SCRIPT_NAV_BAR = """ +<script> + +// Side Navigation functions =================================================== + +function openNav() { + const sidenav = document.getElementById("theSideNav"); + const main = document.getElementById("main"); + + sidenav.style.width = "360px"; + sidenav.style.paddingLeft = "20px"; + main.style.marginLeft = "360px"; + document.getElementById("open-sidenav").style.display = "none"; + document.getElementById("close-sidenav").style.display = "inline-block"; +} + +function closeNav() { + const sidenav = document.getElementById("theSideNav"); + const main = document.getElementById("main"); + + sidenav.style.width = "0"; // Close the sidenav + sidenav.style.paddingLeft = "0"; // Reset padding + main.style.marginLeft = "0"; // Reset page content position + document.getElementById("open-sidenav").style.display = "inline-block"; + document.getElementById("close-sidenav").style.display = "none"; +} + +function toggleFolder(folderHeader) { + const folder = folderHeader.parentElement; + const folderContents = folderHeader.nextElementSibling; + + if (folder.classList.contains("open")) { + folder.classList.remove("open"); + folderContents.style.display = "none"; + } else { + folder.classList.add("open"); + folderContents.style.display = "block"; + } + + // Save the updated state + saveSideNavState(); +} + +function saveSideNavState() { + const sidenav = document.getElementById("theSideNav"); + const isOpen = sidenav.style.width !== "0px"; // Check if sidenav is open + + // Save the state of each folder + const folderStates = Array.from(document.querySelectorAll(".folder")).map(folder => ({ + id: folder.querySelector("span").textContent.trim(), // Use folder name as identifier + isOpen: folder.classList.contains("open") // Check if folder is open + })); + + // Save the sidenav and folder states to localStorage + localStorage.setItem("sidenavState", JSON.stringify({ isOpen, folderStates })); +} + +function restoresidenavState() { + const savedState = localStorage.getItem("sidenavState"); + if (savedState) { + const { isOpen, folderStates } = JSON.parse(savedState); + const sidenav = document.getElementById("theSideNav"); + const main = document.getElementById("main"); + + // Temporarily disable animations on page load so elements don't move + sidenav.style.transition = "none"; + main.style.transition = "none"; + const chevrons = document.querySelectorAll(".chevron"); + chevrons.forEach(chevron => { + chevron.style.transition = "none"; + }); + + // Restore side navigation state + if (isOpen) { + openNav() + } else { + closeNav() + } + + // Restore folder open/closed states + folderStates.forEach(({ id, isOpen }) => { + const folder = Array.from(document.querySelectorAll(".folder")) + .find(folder => folder.querySelector("span").textContent.trim() === id); + + if (folder) { + const folderContents = folder.querySelector(".folder-contents"); + if (isOpen) { + folder.classList.add("open"); + folderContents.style.display = "block"; + } else { + folder.classList.remove("open"); + folderContents.style.display = "none"; + } + } + }); + + // Re-enable animation + setTimeout(() => { + sidenav.style.transition = ""; + main.style.transition = ""; + chevrons.forEach(chevron => { + chevron.style.transition = ""; + }); + }, 100); + } +} + +function saveSidenavScrollPosition() { + const sidenav = document.getElementById("theSideNav"); + const scrollPosition = sidenav.scrollTop; + localStorage.setItem("sidenavScrollPosition", scrollPosition); +} + +function restoreSidenavScrollPosition() { + const sidenav = document.getElementById("theSideNav"); + const savedPosition = localStorage.getItem("sidenavScrollPosition"); + if (savedPosition !== null) { + sidenav.scrollTop = parseInt(savedPosition, 10); + } +} + +function adjustSidenavOffset() { + const topMenu = document.getElementById("topmenu"); + const sidenav = document.getElementById("theSideNav"); + + if (topMenu) { + const topMenuHeight = topMenu.offsetHeight; + document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); + } +} + +adjustSidenavOffset(); +window.addEventListener("resize", adjustSidenavOffset); + +/* This is the scrolling offset when we click on an output in the side navigation bar + It should take into account the top menu height */ +function adjustOutputOffset() { + const topMenu = document.getElementById("topmenu"); // Replace with your top menu's ID + if (topMenu) { + const topMenuHeight = topMenu.offsetHeight; // Dynamically get the top menu height + document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); + } +} + +window.addEventListener("DOMContentLoaded", adjustOutputOffset); +window.addEventListener("resize", adjustOutputOffset); + +// Prevent flickering on page navigation +window.addEventListener("beforeunload", () => { + saveSideNavState(); + saveSidenavScrollPosition(); +}); + +window.addEventListener("load", restoreSidenavScrollPosition); +document.addEventListener("DOMContentLoaded", restoresidenavState); +</script> +""" + +SCRIPT = """ +<script> +// Theme toggle ================================================================ + +function toggleTheme() { + const body = document.body; + + // Check if the current theme is dark + const isDark = body.classList.contains('dark-mode'); + + // Toggle between dark and light themes + if (isDark) { + body.classList.remove('dark-mode'); + body.classList.add('light-mode'); + } else { + body.classList.remove('light-mode'); + body.classList.add('dark-mode'); + } + + // Save the selected theme to localStorage + localStorage.setItem('theme', isDark ? 'light' : 'dark'); +} + +// Do not animate theme toggle on page load +document.addEventListener("DOMContentLoaded", () => { + const themeToggle = document.getElementById('themeToggle'); + const toggleSpan = themeToggle.nextElementSibling; // The <span> element + + // Prevent animation on page load + const savedTheme = localStorage.getItem('theme') || 'dark'; + document.body.classList.add(savedTheme === 'dark' ? 'dark-mode' : 'light-mode'); + themeToggle.checked = savedTheme === 'dark'; + + // Add the "animate" class on user interaction + themeToggle.addEventListener('change', () => { + toggleSpan.classList.add('animate'); + setTimeout(() => { + toggleSpan.classList.remove('animate'); // Remove the animation class after completion + }, 400); // Match the CSS transition duration (0.4s) + }); +}); + +// Avoid flickering of theme toggle on page load +document.addEventListener("DOMContentLoaded", () => { + const body = document.body; + + // Temporarily disable transitions during page load + body.classList.add('no-transition'); + + // Remove the no-transition class after the page is fully loaded + setTimeout(() => { + body.classList.remove('no-transition'); + }, 50); // Allow rendering to complete before enabling transitions +}); + +document.addEventListener('DOMContentLoaded', function () { + const md = window.markdownit({ + html: true, + linkify: true, + typographer: true + }); + + // Find all markdown containers and render them + document.querySelectorAll('.markdown-content').forEach(container => { + const rawMarkdown = container.innerHTML; + container.style.display = 'block'; + container.innerHTML = md.render(rawMarkdown); + }); +}); + +function adjustMainBodyOffset() { + const topMenu = document.getElementById("topmenu"); + const mainBody = document.getElementById("main"); + + if (topMenu && mainBody) { + const topMenuHeight = topMenu.offsetHeight; + mainBody.style.marginTop = `${topMenuHeight}px`; + } +} + +// Apply the adjustment on page load and window resize +window.addEventListener("DOMContentLoaded", adjustMainBodyOffset); +window.addEventListener("resize", adjustMainBodyOffset); + +document.addEventListener("DOMContentLoaded", function () { + const searchBar = document.getElementById("search-bar"); + const autocompleteList = document.getElementById("autocomplete-list"); + const outputLinks = document.querySelectorAll(".sidenav-output"); + + // Collect output names and their hrefs + const outputs = Array.from(outputLinks).map(link => ({ + name: link.textContent.trim(), + href: link.getAttribute("href") + })); + + function updateAutocompleteWidth() { + const searchBarWidth = searchBar.offsetWidth; // Get the width of the search bar + autocompleteList.style.width = `${searchBarWidth}px`; // Match autocomplete width to search bar + } + + // Initial width adjustment + updateAutocompleteWidth(); + + // Update width on window resize to handle responsiveness + window.addEventListener("resize", updateAutocompleteWidth); + + + // Update autocomplete suggestions + function updateAutocomplete(query) { + // Clear previous suggestions + autocompleteList.innerHTML = ""; + + // Filter and display matching outputs + const matches = outputs.filter(output => + output.name.toLowerCase().includes(query.toLowerCase()) + ); + + if (matches.length > 0) { + matches.forEach(match => { + const listItem = document.createElement("li"); + listItem.textContent = match.name; + listItem.addEventListener("click", () => { + window.location.href = match.href; // Redirect to the selected output + }); + autocompleteList.appendChild(listItem); + }); + autocompleteList.style.display = "block"; // Show the list + } else { + autocompleteList.style.display = "none"; // Hide the list if no matches + } + } + + // Add event listeners to the search bar + searchBar.addEventListener("input", () => { + const query = searchBar.value.trim(); + if (query) { + updateAutocomplete(query); + } else { + autocompleteList.innerHTML = ""; // Clear suggestions + autocompleteList.style.display = "none"; // Hide the list + } + }); + + // Hide autocomplete list when clicking outside + document.addEventListener("click", (event) => { + if (!searchBar.contains(event.target) && !autocompleteList.contains(event.target)) { + autocompleteList.style.display = "none"; + } + }); +}); +</script> +""" + +def _run_command(cmd): + logger.debug('- Executing: '+GS.pasteable_cmd(cmd)) + try: + cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + if e.output: + logger.debug('Output from command: '+e.output.decode()) + logger.non_critical_error(f'Failed to run {cmd[0]}, error {e.returncode}') + return False + if cmd_output.strip(): + logger.debug('- Output from command:\n'+cmd_output.decode()) + return True + + +class Navigate_Results_ModernOptions(BaseOptions): + def __init__(self): + with document: + self.output = GS.def_global_output + """ *Filename for the output (%i=html, %x=navigate) """ + self.link_from_root = '' + """ *The name of a file to create at the main output directory linking to the home page """ + self.skip_not_run = False + """ Skip outputs with `run_by_default: false` """ + self.logo = Optionable + """ [string|boolean=''] PNG file to use as logo, use false to remove. + The KiBot logo is used by default """ + self.logo_url = 'https://github.com/INTI-CMNB/KiBot/' + """ Target link when clicking the logo """ + self.title = '' + """ Title for the page, when empty KiBot will try using the schematic or PCB title. + If they are empty the name of the project, schematic or PCB file is used. + You can use %X values and KiCad variables here """ + self.title_url = Optionable + """ [string|boolean=''] Target link when clicking the title, use false to remove. + KiBot will try with the origin of the current git repo when empty """ + self.nav_bar = True + """ Add a side navigation bar to quickly access to the outputs """ + self.render_markdown = True + """ If True, markdown files are rendered; otherwise, they are treated like other files """ + super().__init__() + self._expand_id = 'navigate' + self._expand_ext = 'html' + self._variant_name = self._find_variant_name() + def config(self, parent): + super().config(parent) + # Logo + if isinstance(self.logo, bool): + self.logo = '' if self.logo else None + elif self.logo: + self.logo = os.path.abspath(self.logo) + if not os.path.isfile(self.logo): + raise KiPlotConfigurationError('Missing logo file `{}`'.format(self.logo)) + try: + self._logo_data, _, _, _ = read_png(self.logo, logger) + except TypeError as e: + raise KiPlotConfigurationError(f'Only PNG images are supported for the logo ({e})') + if self.logo == '': + # Internal logo + self._logo_data = base64.b64decode(KIBOT_LOGO) + elif self.logo is None: + self._logo_data = '' + # Title URL + if isinstance(self.title_url, bool): + self.title_url = '' if self.title_url else None + + def add_to_tree(self, cat, out, o_tree): + # Add `out` to `o_tree` in the `cat` category + if cat == '.': + # Place output directly at the root level + o_tree[out.name] = out + else: + cat = cat.split('/') + node = o_tree + for c in cat: + if c not in node: + # New category + node[c] = {} + node = node[c] + node[out.name] = out + + def svg_to_png(self, svg_file, png_file, width): + cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', png_file, svg_file] + return _run_command(cmd) + + def copy(self, img, width): + """ Copy an SVG icon to the images/ dir. + Tries to convert it to PNG. """ + img_w = "{}_{}".format(os.path.basename(img), width) + if img_w in self.copied_images: + # Already copied, just return its name + return self.copied_images[img_w] + src = os.path.join(self.img_src_dir, img+'.svg') if not img.endswith('.svg') else img + dst = os.path.join(self.out_dir, 'images', img_w) + id = img_w + if self.rsvg_command is not None and self.svg_to_png(src, dst+'.png', width): + img_w += '.png' + else: + copy2(src, dst+'.svg') + img_w += '.svg' + name = os.path.join('images', img_w) + self.copied_images[id] = name + return name + + def can_be_converted(self, ext): + if ext in IMAGEABLES_SVG and self.rsvg_command is None: + logger.warning(W_MISSTOOL+"Missing SVG to PNG converter") + return False + if ext in IMAGEABLES_GS and not self.ps2img_avail: + logger.warning(W_MISSTOOL+"Missing PS/PDF to PNG converter") + return False + if ext in IMAGEABLES_SIMPLE and self.convert_command is None: + logger.warning(W_MISSTOOL+"Missing ImageMagick converter") + return False + return ext in IMAGEABLES_SVG or ext in IMAGEABLES_GS or ext in IMAGEABLES_SIMPLE + + def compose_image(self, file, ext, img, out_name, no_icon=False): + if not os.path.isfile(file): + logger.warning(W_NOTYET+"{} not yet generated, using an icon".format(os.path.relpath(file))) + return False, None, None + if self.convert_command is None: + return False, None, None + # Create a unique name using the output name and the generated file name + bfname = os.path.splitext(os.path.basename(file))[0] + fname = os.path.join(self.out_dir, 'images', out_name+'_'+bfname+'.png') + # Full path for the icon image + icon = os.path.join(self.out_dir, img) + if ext == 'pdf': + # Only page 1 + file += '[0]' + if ext == 'svg': + tmp_name = GS.tmp_file(suffix='.png') + logger.debug('Temporal convert: {} -> {}'.format(file, tmp_name)) + if not self.svg_to_png(file, tmp_name, BIG_ICON): + return False, None, None + file = tmp_name + cmd = [self.convert_command, file, + # Size for the big icons (width) + '-resize', str(BIG_ICON)+'x'] + if ext == 'ps': + # ImageMagick 6.9.11 (and also the one in Debian 11) rotates the PS + cmd.extend(['-rotate', '90']) + if not no_icon: + cmd.extend([ # Add the file type icon + icon, + # At the bottom right + '-gravity', 'south-east', + # This is a composition, not 2 images + '-composite']) + cmd.append(fname) + res = _run_command(cmd) + if ext == 'svg': + logger.debug('Removing temporal {}'.format(tmp_name)) + os.remove(tmp_name) + return res, fname, os.path.relpath(fname, start=self.out_dir) + + def get_image_for_file(self, file, out_name, no_icon=False, image=None): + ext = os.path.splitext(file)[1][1:].lower() + wide = False + # Copy the icon for this file extension + icon_name = 'folder' if os.path.isdir(file) else EXT_IMAGE.get(ext, 'unknown') + img = self.copy(image or icon_name, MID_ICON) + # Full name for the file + file_full = file + # Just the file, to display it + file = os.path.basename(file) + # The icon size + height = width = MID_ICON + # Check if this file can be represented by an image + if self.can_be_converted(ext): + # Try to compose the image of the file with the icon + ok, fimg, new_img = self.compose_image(file_full, ext, img, 'cat_'+out_name, no_icon) + if ok: + # It was converted, replace the icon by the composited image + img = new_img + # Compute its size + try: + _, width, height, _ = read_png(fimg, logger) + except TypeError: + width = height = 0 + # We are using the big size + wide = True + # Now add the image with its file name as caption + ext_img = '<img src="{}" alt="{}" width="{}" height="{}">'.format(img, file, width, height) + file = ('<table class="out-img"><tr><td>{}</td></tr><tr><td class="{}">{}</td></tr></table>'. + format(ext_img, 'td-normal' if no_icon else 'td-small', out_name if no_icon else file)) + return file, wide + + def add_nav_bar(self, f, prev): + if self.nav_bar: + f.write(SCRIPT_NAV_BAR) + + def write_head(self, f, title): + f.write('<!DOCTYPE html>\n') + f.write('<html lang="en">\n') + f.write('<head>\n') + f.write(' <title>{}\n'.format(title if title else 'Main page')) + f.write(' \n') # UTF-8 encoding for unicode support + f.write(' \n') + f.write(' \n') + # Include Markdown-it + f.write(' \n') + f.write('\n') + f.write('\n') + f.write(self.sidenav) + f.write(self.top_menu) + f.write('
\n') + + def generate_cat_page_for(self, name, node, prev, category): + logger.debug('- Categories: ' + str(node.keys())) + with open(os.path.join(self.out_dir, name), 'wt') as f: + self.write_head(f, category) + name, ext = os.path.splitext(name) + + # Start a vertically aligned container for categories + f.write('
\n') + + for cat, content in node.items(): + if not isinstance(content, dict): + continue + + pname = name + '_' + cat + ext + self.generate_page_for(content, pname, name, category + '/' + cat) + + # Wrap the entire box in the tag to make it clickable + f.write(f''' + + {cat} + + ''') + + # Close the container + f.write('
\n') + + # Generate outputs below the categories + self.generate_outputs(f, node) + self.add_nav_bar(f, prev) + f.write(SCRIPT) + f.write('\n\n') + + def adjust_image_paths(self, md_content, current_dir, html_output_dir): + """ + Adjusts image paths in markdown content to be relative to the HTML output directory. + + Args: + md_content (str): Raw markdown content. + current_dir (str): Directory of the markdown file. + html_output_dir (str): Absolute directory where the HTML file is generated. + + Returns: + str: Updated markdown content with adjusted image paths. + """ + import re + + image_pattern = r'!\[.*?\]\((.*?)\)' # Markdown image paths: ![Alt text](path/to/image.png) + html_img_pattern = r']*src="([^"]+)"' # HTML img src="path/to/image.png" + + def replace_path(match): + original_path = match.group(1) + + # Skip absolute URLs or paths + if original_path.startswith(('http://', 'https://', '/')): + return match.group(0) + + # Convert relative path to absolute and back to relative for the HTML output directory + abs_path = os.path.abspath(os.path.join(current_dir, original_path)) + rel_path = os.path.relpath(abs_path, html_output_dir) + + # Replace the path with the new relative path + return match.group(0).replace(original_path, rel_path) + + # Replace Markdown image paths + md_content = re.sub(image_pattern, replace_path, md_content) + + # Replace HTML tag paths + md_content = re.sub(html_img_pattern, replace_path, md_content) + + return md_content + + def generate_outputs(self, f, node): + """ + Generates the output table dynamically, grouping items into separate categories. + Respects the `render_markdown` option for markdown files. + """ + for oname, out in node.items(): + if isinstance(out, dict): + continue # Skip subcategories here, handled separately + + # Start a container for the category + f.write(f'
\n') # Virtual box used to hold id + f.write('
{}
\n'.format(out.comment or oname)) + + out_dir = get_output_dir(out.dir, out, dry=True) + targets, icons = out.get_navigate_targets(out_dir) + + # Start the items container + f.write('
\n') + + for tg, icon in zip(targets, icons if icons else [None] * len(targets)): + tg_rel = os.path.relpath(os.path.abspath(tg), start=self.out_dir) + ext = os.path.splitext(tg)[1].lower() + + if ext == '.md' and self.render_markdown: # Render markdown only if enabled + # Read markdown content + with open(tg, 'r', encoding='utf-8') as md_file: + md_content = md_file.read() + + # Adjust image paths in markdown + md_content = self.adjust_image_paths(md_content, os.path.dirname(tg), self.out_dir) + + # Embed raw markdown into a div + f.write(f''' + + ''') + else: + # Handle other files (icons, images, etc.) + img, wide = self.get_image_for_file(tg, oname, image=icon) + + cell_class = "wide" if wide else "" + + # Make the entire output-box clickable + f.write(f''' +
+ {img} +

{oname}

+
+ ''') + + # Close the items container and category box + f.write('
\n
\n') + + def generate_end_page_for(self, name, node, prev, category): + logger.debug('- Outputs: '+str(node.keys())) + with open(os.path.join(self.out_dir, name), 'wt') as f: + self.write_head(f, category) + name, ext = os.path.splitext(name) + self.generate_outputs(f, node) + self.add_nav_bar(f, prev) + f.write(SCRIPT) + f.write('\n\n') + + def generate_page_for(self, node, name, prev=None, category=''): + logger.debug('Generating page for ' + name) + self.top_menu = self.generate_top_menu(category) # Update top menu with the current folder name + if isinstance(list(node.values())[0], dict): + self.generate_cat_page_for(name, node, prev, category) + else: + self.generate_end_page_for(name, node, prev, category) + + def get_targets(self, out_dir): + # Listing all targets is too complex, we list the most relevant + # This is good enough to compress the result + name = self._parent.expand_filename(out_dir, self.output) + files = [os.path.join(out_dir, 'images'), + os.path.join(out_dir, 'styles.css'), + os.path.join(out_dir, 'favicon.ico')] + if self.link_from_root: + files.append(os.path.join(GS.out_dir, self.link_from_root)) + self.out_dir = out_dir + self.get_html_names(self.create_tree(), name, files) + return files + + def get_html_names_cat(self, name, node, prev, category, files): + files.append(os.path.join(self.out_dir, name)) + name, ext = os.path.splitext(name) + for cat, content in node.items(): + if not isinstance(content, dict): + continue + pname = name+'_'+cat+ext + self.get_html_names(content, pname, files, name, category+'/'+cat) + + def get_html_names(self, node, name, files, prev=None, category=''): + if isinstance(list(node.values())[0], dict): + self.get_html_names_cat(name, node, prev, category, files) + else: + files.append(os.path.join(self.out_dir, name)) + + def create_tree(self): + o_tree = {} + BasePreFlight.configure_all() + for n in BasePreFlight.get_in_use_names(): + pre = BasePreFlight.get_preflight(n) + cat = force_list(pre.get_category()) + if not cat: + continue + for c in cat: + self.add_to_tree(c, pre, o_tree) + for o in RegOutput.get_outputs(): + if not o.run_by_default and self.skip_not_run: + # Skip outputs that aren't generated in a regular run + continue + config_output(o) + cat = o.category + if cat is None: + continue + for c in cat: + self.add_to_tree(c, o, o_tree) + return o_tree + + def generate_sidenav_one(self, node, lvl, name, ext): + indent = ' ' * lvl + code = f"{indent}
    \n" + for k, v in node.items(): + if isinstance(v, dict): # Folder (category) + folder_id = f'folder-{name}-{k}'.replace(' ', '-').lower() + code += ( + f"{indent}
  • " + f"" + f" {k}\n" + f"{indent} \n
  • \n" + else: # File (output) + code += f"{indent}
  • {v.name}
  • \n" + code += f"{indent}
\n" + return code + + def generate_sidenav(self, node, name): + name, ext = os.path.splitext(name) + code = ''' +
+ +
+ +
    +
    + ''' + code += self.generate_sidenav_one(node, 0, name, ext) + code += '
    \n' + code += """ + + """ + return code + + def generate_top_menu(self, category=''): + """ + Generates the top menu with Back, Forward, Home, sidenav buttons, and a logo. + Displays the current folder name dynamically next to the forward arrow with wrapping for '/'. + Adds revision, variant, and company information. + """ + fsize = f'{TITLE_HEIGHT}px' + logo_height = f'{TITLE_HEIGHT}px' + small_font_size = f'{int(TITLE_HEIGHT) - 8}px' # Smaller font for revision/variant and company + smallest_font_size = f'{int(TITLE_HEIGHT) - 16}px' + code = '
    \n' + code += ' \n' + code += ' \n' + + # Left-aligned section (sidenav button, Back/Forward buttons, and category path) + code += ' \n' + + # Centered title with company below + code += ' \n' + + # Right-aligned section (Logo, Revision/Variant, Toggle, and Home button) + code += ' \n' + + code += ' \n' + code += '
    \n' + if self.nav_bar: + code += f' \n' + code += f' \n' + code += f' \n' + code += f' \n' + + # Display the current folder name (category path) + if category: # Display the current folder name + code += f''' + + {category.lstrip('/').replace('/', '/')} + + ''' + code += ' \n' + if self._solved_title: + if self.title_url: + code += f' \n' + code += f' {self._solved_title}\n' + if self.title_url: + code += ' \n' + # Add company information + code += f''' +
    + {self._solved_company} +
    + ''' + code += '
    \n' + + # Revision and Variant + code += f''' +
    +
    Rev. {self._solved_revision}
    + Variant: {self._variant_name} +
    + ''' + + # Add the logo + if self.logo is not None: + img_name = os.path.join('images', 'logo.png') + if self.logo_url: + code += f' \n' + code += f' Logo\n' + if self.logo_url: + code += ' \n' + + # Add the toggle + code += ''' + + ''' + code += f' \n' + code += '
    \n' + code += '
    \n' + return code + + def solve_title(self): + base_title = None + if GS.sch: + base_title = GS.sch.get_title() + if GS.board and not base_title: + tb = GS.board.GetTitleBlock() + base_title = tb.GetTitle() + if not base_title: + base_title = GS.pro_basename or GS.sch_basename or GS.pcb_basename or 'Unknown' + text = self.expand_filename_sch(self.title if self.title else '+') + if text[0] == '+': + text = base_title+text[1:] + self._solved_title = text + # Now the URL + if self.title_url is not None and not self.title_url: + # Empty but not None + self._git_command = self.check_tool('Git') + if self._git_command: + res = '' + try: + res = run_command([self._git_command, 'remote', 'get-url', 'origin'], just_raise=True) + except subprocess.CalledProcessError: + pass + if res: + self.title_url = res + + def solve_revision(self): + base_rev = None + if GS.sch: + GS.load_sch() + GS.load_sch_title_block() + base_rev = GS.sch_rev + if GS.board and not base_rev: + tb = GS.board.GetTitleBlock() + base_rev = tb.GetRevision() + if not base_rev: + base_rev = 'Unknown' + self._solved_revision = base_rev + + def solve_company(self): + base_comp = None + if GS.sch: + GS.load_sch() + GS.load_sch_title_block() + base_comp = GS.sch_comp + if GS.board and not base_comp: + tb = GS.board.GetTitleBlock() + base_comp = tb.GetCompany() + if not base_comp: + base_comp = 'Unknown' + self._solved_company = base_comp + + def run(self, name): + self.out_dir = os.path.dirname(name) + self.img_src_dir = GS.get_resource_path('images') + self.img_dst_dir = os.path.join(self.out_dir, 'images') + os.makedirs(self.img_dst_dir, exist_ok=True) + self.copied_images = {} + name = os.path.basename(name) + # Create a tree with all the outputs + o_tree = self.create_tree() + logger.debug('Collected outputs:\n'+pprint.pformat(o_tree)) + if not o_tree: + logger.warning(W_NOOUTPUTS+'No outputs for navigate results') + return + with open(os.path.join(self.out_dir, 'styles.css'), 'wt') as f: + top_margin = TITLE_HEIGHT + f.write(STYLE.replace('@TOP_MAR@', str(top_margin))) + self.rsvg_command = self.check_tool('rsvg1') + self.convert_command = self.check_tool('ImageMagick') + self.ps2img_avail = self.check_tool('Ghostscript') + # Create the pages + self.home = name + self.back_img = self.copy('back', MID_ICON) + self.home_img = self.copy('home', MID_ICON) + copy2(os.path.join(self.img_src_dir, 'favicon.ico'), os.path.join(self.out_dir, 'favicon.ico')) + # Copy the logo image + if self.logo is not None: + with open(os.path.join(self.out_dir, 'images', 'logo.png'), 'wb') as f: + f.write(self._logo_data) + self.solve_title() + self.solve_revision() + self.solve_company() + self.sidenav = self.generate_sidenav(o_tree, name) if self.nav_bar else '' + self.top_menu = self.generate_top_menu() + self.generate_page_for(o_tree, name) + # Link it? + if self.link_from_root: + redir_file = os.path.join(GS.out_dir, self.link_from_root) + rel_start = os.path.relpath(os.path.join(self.out_dir, name), start=GS.out_dir) + logger.debug('Creating redirector: {} -> {}'.format(redir_file, rel_start)) + with open(redir_file, 'wt') as f: + f.write('\n\n'.format(rel_start)) + f.write('\n') + + +@output_class +class Navigate_Results_Modern(BaseOutput): # noqa: F821 + """ Navigate Results + Generates a web page to navigate the generated outputs """ + def __init__(self): + super().__init__() + # Make it low priority so it gets created after all the other outputs + self.priority = 10 + with document: + self.options = Navigate_Results_ModernOptions + """ *[dict={}] Options for the `navigate_results_modern` output """ + # The help is inherited and already mentions the default priority + self.fix_priority_help() + self._any_related = True + + @staticmethod + def get_conf_examples(name, layers): + outs = BaseOutput.simple_conf_examples(name, 'Web page to browse the results', 'Browse') # noqa: F821 + outs[0]['options'] = {'link_from_root': 'index.html', 'skip_not_run': True} + return outs From 69fb899d0aa9cf3b8bef027b7fbaa819b53c9896 Mon Sep 17 00:00:00 2001 From: Vincent Nguyen Date: Sat, 11 Jan 2025 00:05:08 +0100 Subject: [PATCH 02/14] [Navigate Results Modern][Fixed] pre-commit hooks --- kibot/out_navigate_results_modern.py | 687 ++++++++++++++------------- 1 file changed, 347 insertions(+), 340 deletions(-) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index b386750f8..17e92bc68 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -24,11 +24,9 @@ """ import base64 import os -import re import subprocess import pprint from shutil import copy2 -from math import ceil from .bom.kibot_logo import KIBOT_LOGO from .error import KiPlotConfigurationError from .gs import GS @@ -38,7 +36,7 @@ from .pre_base import BasePreFlight from .registrable import RegOutput from .macros import macros, document, output_class # noqa: F401 -from . import log, __version__ +from . import log logger = log.get_logger() EXT_IMAGE = {'gbr': 'file_gbr', @@ -107,46 +105,46 @@ /* Colors =================================================================== */ :root { - --light-bg-color: #ffffff; - --dark-bg-color: #1e1e2f; - --light-bg-color-banner: #dfdfdf; - --dark-bg-color-banner: #27293d; - --light-text-color: #444444; - --dark-text-color: #e5e5e5; - --light-hover-color: #902ec9; - --light-hover-color-act: #652f85; - --dark-hover-color: #ffa500; - --dark-hover-color-act: #cc8400; - --dark-text-color-accent: #a3a3c2; - --light-text-color-accent: #444444; - --light-banner-hover: #b0b0b0; - --dark-banner-hover: #383b4b; - --text-color-accent: #a3a3c2; + --light-bg-color: #ffffff; + --dark-bg-color: #1e1e2f; + --light-bg-color-banner: #dfdfdf; + --dark-bg-color-banner: #27293d; + --light-text-color: #444444; + --dark-text-color: #e5e5e5; + --light-hover-color: #902ec9; + --light-hover-color-act: #652f85; + --dark-hover-color: #ffa500; + --dark-hover-color-act: #cc8400; + --dark-text-color-accent: #a3a3c2; + --light-text-color-accent: #444444; + --light-banner-hover: #b0b0b0; + --dark-banner-hover: #383b4b; + --text-color-accent: #a3a3c2; } /* Main body ================================================================ */ body { - margin: 0; - font-family: 'Roboto', sans-serif; - background-color: var(--dark-bg-color); - color: var(--dark-text-color); - transition: + margin: 0; + font-family: 'Roboto', sans-serif; + background-color: var(--dark-bg-color); + color: var(--dark-text-color); + transition: background-color 0.4s ease, color 0.4s ease, transition: scrollbar-color 0.2s ease-in-out; } body.dark-mode { - --text-color-accent: var(--dark-text-color-accent); - background-color: var(--dark-bg-color); - color: var(--dark-text-color); + --text-color-accent: var(--dark-text-color-accent); + background-color: var(--dark-bg-color); + color: var(--dark-text-color); } body.light-mode { - --text-color-accent: var(--light-text-color-accent); - background-color: var(--light-bg-color); - color: var(--light-text-color); + --text-color-accent: var(--light-text-color-accent); + background-color: var(--light-bg-color); + color: var(--light-text-color); } /* Top Menu ================================================================= */ @@ -155,26 +153,26 @@ /* [X/☰] [↩] [↪] (Logo) [☾/☀] [🏠︎] */ #topmenu { - position: fixed; - top: 0; - left: 0; - width: 100%; - z-index: 1000; - background-color: var(--dark-bg-color-banner); - padding: 10px 0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); - display: flex; - align-items: center; - justify-content: space-between; + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 1000; + background-color: var(--dark-bg-color-banner); + padding: 10px 0; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: space-between; transition: background-color 0.2s ease, color 0.2s ease; } body.light-mode #topmenu { - background-color: var(--light-bg-color-banner); + background-color: var(--light-bg-color-banner); } body.dark-mode #topmenu { - background-color: var(--dark-bg-color-banner); + background-color: var(--dark-bg-color-banner); } /* Buttons ================================================================== */ @@ -182,75 +180,75 @@ /* button corresponds to the navigation buttons (forward, backward, home) */ button, #open-sidenav, #close-sidenav { - background: none; - border: none; - color: var(--dark-text-color); - cursor: pointer; - transition: color 0.3s ease; + background: none; + border: none; + color: var(--dark-text-color); + cursor: pointer; + transition: color 0.3s ease; } body.light-mode #topmenu button, body.light-mode #topmenu #open-sidenav, body.light-mode #topmenu #close-sidenav { - color: var(--light-text-color); + color: var(--light-text-color); } body.dark-mode #topmenu button, body.dark-mode #topmenu #open-sidenav, body.dark-mode #topmenu #close-sidenav { - color: var(--dark-text-color); + color: var(--dark-text-color); } button { - font-size: 20px; - margin: 0 10px; + font-size: 20px; + margin: 0 10px; } #open-sidenav, #close-sidenav { - width: 36px; - height: 36px; - line-height: 36px; - text-align: center; - font-size: 28px; - margin-left: 10px; - user-select: none; /* Prevent text selection */ + width: 36px; + height: 36px; + line-height: 36px; + text-align: center; + font-size: 28px; + margin-left: 10px; + user-select: none; /* Prevent text selection */ } /* Hover effects */ button:hover, #open-sidenav:hover, #close-sidenav:hover { - color: var(--dark-hover-color); + color: var(--dark-hover-color); } body.dark-mode #topmenu button:hover, body.dark-mode #topmenu #open-sidenav:hover, body.dark-mode #topmenu #close-sidenav:hover { - color: var(--dark-hover-color); + color: var(--dark-hover-color); } body.light-mode #topmenu button:hover, body.light-mode #topmenu #open-sidenav:hover, body.light-mode #topmenu #close-sidenav:hover { - color: var(--light-hover-color); + color: var(--light-hover-color); } /* Active effects */ button:active, #open-sidenav:active, #close-sidenav:active { - color: var(--dark-hover-color-act); - transition: none; + color: var(--dark-hover-color-act); + transition: none; } body.dark-mode #topmenu button:active, body.dark-mode #topmenu #open-sidenav:active, body.dark-mode #topmenu #close-sidenav:active { - color: var(--dark-hover-color-act); + color: var(--dark-hover-color-act); } body.light-mode #topmenu button:active, body.light-mode #topmenu #open-sidenav:active, body.light-mode #topmenu #close-sidenav:active { - color: var(--light-hover-color-act); + color: var(--light-hover-color-act); } /* Sidebar Navigation ======================================================= */ @@ -264,8 +262,8 @@ background-color: #27293d; overflow-x: hidden; overflow-y: auto; - transition: - width 0.5s ease, + transition: + width 0.5s ease, padding-left 0.5s ease, scrollbar-color 0.2s ease-in-out, background-color 0.2s ease-in-out; @@ -275,217 +273,217 @@ body.dark-mode .sidenav { - background-color: var(--dark-bg-color-banner); + background-color: var(--dark-bg-color-banner); } body.light-mode .sidenav { - background-color: var(--light-bg-color-banner); + background-color: var(--light-bg-color-banner); } .sidenav > ul:first-child { - margin-top: 20px; /* Padding between top menu and first element of sidenav */ + margin-top: 20px; /* Padding between top menu and first element of sidenav */ } -/* Side Navigation Outputs -------------------------------------------------- */ +/* Side Navigation Outputs -------------------------------------------------- */ .sidenav-output { - padding: 8px 30px; - text-decoration: none; - font-size: 16px; - color: var(--dark-text-color); - display: block; - transition: color 0.3s ease; - border-radius: 4px; + padding: 8px 30px; + text-decoration: none; + font-size: 16px; + color: var(--dark-text-color); + display: block; + transition: color 0.3s ease; + border-radius: 4px; } body.light-mode .sidenav-output { - color: var(--light-text-color); + color: var(--light-text-color); } body.dark-mode .sidenav-output { - color: var(--dark-text-color); + color: var(--dark-text-color); } /* Hover effects */ .sidenav-output:hover { - color: var(--dark-hover-color); - background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); } body.dark-mode .sidenav-output:hover { - color: var(--dark-hover-color); - background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); } body.light-mode .sidenav-output:hover { - color: var(--light-hover-color); - background-color: var(--light-banner-hover); + color: var(--light-hover-color); + background-color: var(--light-banner-hover); } /* Active effects */ .sidenav-output:active { - color: var(--dark-hover-color-act); + color: var(--dark-hover-color-act); } body.dark-mode .sidenav-output:active { - color: var(--dark-hover-color-act); + color: var(--dark-hover-color-act); } body.light-mode .sidenav-output:active { - color: var(--light-hover-color-act); + color: var(--light-hover-color-act); } /* Side Navigation Categories ----------------------------------------------- */ .sidenav-category { - list-style: none; - padding: 0; - margin: 0; - user-select: none; /* Prevent text selection */ + list-style: none; + padding: 0; + margin: 0; + user-select: none; /* Prevent text selection */ } .sidenav-category .folder > span { - display: flex; - align-items: center; - cursor: pointer; - color: var(--dark-text-color-accent); - padding: 10px 20px; - margin-bottom: 0px; - width: 100%; - transition: background-color 0.3s, color 0.3s; - border-radius: 4px; + display: flex; + align-items: center; + cursor: pointer; + color: var(--dark-text-color-accent); + padding: 10px 20px; + margin-bottom: 0px; + width: 100%; + transition: background-color 0.3s, color 0.3s; + border-radius: 4px; } .sidenav-category .folder-contents { - list-style: none; - margin-left: 20px; - padding: 0; + list-style: none; + margin-left: 20px; + padding: 0; } body.dark-mode .sidenav-category .folder > span { - color: var(--dark-text-color-accent); + color: var(--dark-text-color-accent); } body.light-mode .sidenav-category .folder > span { - color: var(--light-text-color); + color: var(--light-text-color); } /* Hover effects */ .sidenav-category .folder > span:hover { - background-color: var(--dark-banner-hover); - color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); } body.dark-mode .sidenav-category .folder > span:hover { - color: var(--dark-hover-color); - background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); } body.light-mode .sidenav-category .folder > span:hover { - color: var(--light-hover-color); - background-color: var(--light-banner-hover); + color: var(--light-hover-color); + background-color: var(--light-banner-hover); } /* Active effects */ .sidenav-category .folder > span:active { - color: var(--dark-hover-color); - transition: none; + color: var(--dark-hover-color); + transition: none; } body.dark-mode .sidenav-category .folder > span:active { - color: var(--dark-hover-color-act); + color: var(--dark-hover-color-act); } body.light-mode .sidenav-category .folder > span:active { - color: var(--light-hover-color-act); + color: var(--light-hover-color-act); } /* Chevron (arrow) styling -------------------------------------------------- */ .chevron { - display: block; - width: 0; - height: 0; - border: 8px solid transparent; - border-left-color: #606077; - margin-right: 8px; - transform-origin: 25% 50%; - transition: transform 0.3s ease, border-left-color 0.3s ease; - pointer-events: none; + display: block; + width: 0; + height: 0; + border: 8px solid transparent; + border-left-color: #606077; + margin-right: 8px; + transform-origin: 25% 50%; + transition: transform 0.3s ease, border-left-color 0.3s ease; + pointer-events: none; } body.dark-mode .chevron { - border-left-color: #606077; + border-left-color: #606077; } body.light-mode .chevron { - border-left-color: #909090; + border-left-color: #909090; } /* We change styles for when the chevron is pointing down */ .folder.open > span .chevron { - border-left-color: var(--dark-text-color-accent); - transform: rotate(90deg); + border-left-color: var(--dark-text-color-accent); + transform: rotate(90deg); } body.dark-mode .folder.open > span .chevron { - border-left-color: var(--dark-text-color-accent); + border-left-color: var(--dark-text-color-accent); } body.light-mode .folder.open > span .chevron { - border-left-color: var(--light-text-color-accent); + border-left-color: var(--light-text-color-accent); } /* Hover effects */ body.dark-mode .folder > span:hover .chevron { - border-left-color: var(--dark-hover-color) + border-left-color: var(--dark-hover-color) } body.light-mode .folder > span:hover .chevron { - border-left-color: var(--light-hover-color) + border-left-color: var(--light-hover-color) } /* Active effects */ body.dark-mode .folder > span:active .chevron { - border-left-color: var(--dark-hover-color-act) + border-left-color: var(--dark-hover-color-act) } body.light-mode .folder > span:active .chevron { - border-left-color: var(--light-hover-color-act) + border-left-color: var(--light-hover-color-act) } /* Main content ============================================================= */ #main { - transition: margin-left 0.5s; - padding: 16px; - margin-top: 80px; + transition: margin-left 0.5s; + padding: 16px; + margin-top: 80px; } /* Comment field of output is used as a title for each output */ .output-comment { - font-size: 1.4em; - font-weight: 500; - color: var(--dark-text-color); - margin: 20px 0 10px 0; - text-align: center; + font-size: 1.4em; + font-weight: 500; + color: var(--dark-text-color); + margin: 20px 0 10px 0; + text-align: center; } body.light-mode .output-comment { - color: var(--light-text-color); + color: var(--light-text-color); } body.dark-mode .output-comment { - color: var(--dark-text-color); + color: var(--dark-text-color); } /* Category boxes (folder) -------------------------------------------------- */ @@ -506,16 +504,16 @@ } body.light-mode .category-box { - color: var(--light-text-color); - background-color: var(--light-bg-color-banner); - border: var(--light-bg-color-banner); + color: var(--light-text-color); + background-color: var(--light-bg-color-banner); + border: var(--light-bg-color-banner); } body.dark-mode .category-box { - color: var(--dark-text-color); - background-color: var(--dark-bg-color-banner); - border: var(--dark-bg-color-banner); + color: var(--dark-text-color); + background-color: var(--dark-bg-color-banner); + border: var(--dark-bg-color-banner); } .category-title { @@ -528,44 +526,44 @@ } body.light-mode .category-title { - color: var(--light-text-color); + color: var(--light-text-color); } body.dark-mode .category-title { - color: var(--dark-text-color); + color: var(--dark-text-color); } /* Hover effects */ .category-box:hover { - background-color: var(--dark-banner-hover); - transform: scale(1.05); /* Slight zoom effect */ - cursor: pointer; + background-color: var(--dark-banner-hover); + transform: scale(1.05); /* Slight zoom effect */ + cursor: pointer; } body.light-mode .category-box:hover { - background-color: var(--light-banner-hover); + background-color: var(--light-banner-hover); } body.dark-mode .category-box:hover { - background-color: var(--dark-banner-hover); + background-color: var(--dark-banner-hover); } /* Output boxes (files) ----------------------------------------------------- */ .output-box { - background-color: var(--dark-bg-color-banner); - border: 1px solid var(--dark-bg-color-banner); - border-radius: 8px; - padding: 16px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 300px; - height: 140px; - text-decoration: none; - transition: background-color 0.3s ease, transform 0.2s ease; + background-color: var(--dark-bg-color-banner); + border: 1px solid var(--dark-bg-color-banner); + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 300px; + height: 140px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; } /* Offset the scroll position */ @@ -578,143 +576,143 @@ /* Some files (e.g. PDF, PNG) have wider output boxes */ .output-box.wide { - width: 400px; - height: auto; + width: 400px; + height: auto; } .output-box img { - max-width: 100%; - max-height: 100%; - height: auto; - margin-bottom: 10px; + max-width: 100%; + max-height: 100%; + height: auto; + margin-bottom: 10px; } /* The output boxes are centered and wrap around */ .items-container { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 20px; - padding: 20px; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + padding: 20px; } body.light-mode .output-box { - color: var(--light-text-color); - background-color: var(--light-bg-color-banner); - border: var(--light-bg-color-banner); + color: var(--light-text-color); + background-color: var(--light-bg-color-banner); + border: var(--light-bg-color-banner); } body.dark-mode .output-box { - color: var(--dark-text-color); - background-color: var(--dark-bg-color-banner); - border: var(--dark-bg-color-banner); + color: var(--dark-text-color); + background-color: var(--dark-bg-color-banner); + border: var(--dark-bg-color-banner); } /* Hover effects */ .output-box:hover { - background-color: var(--dark-banner-hover); - transform: scale(1.05); - cursor: pointer; + background-color: var(--dark-banner-hover); + transform: scale(1.05); + cursor: pointer; } body.light-mode .output-box:hover { - background-color: var(--light-banner-hover); + background-color: var(--light-banner-hover); } body.dark-mode .output-box:hover { - background-color: var(--dark-banner-hover); + background-color: var(--dark-banner-hover); } /* Name of the output below the icon */ .output-box .output-name { - color: #8997c6; - font-size: 14px; - margin-top: 8px; - text-align: center; + color: #8997c6; + font-size: 14px; + margin-top: 8px; + text-align: center; } body.light-mode .output-box .output-name { - color: var(--light-text-color-accent); + color: var(--light-text-color-accent); } body.dark-mode .output-box .output-name { - color: #8997c6; + color: #8997c6; } /* Filename below the icon */ .output-box .filename { - text-decoration: none; - color: var(--dark-text-color); - text-align: center; - font-size: 14px; + text-decoration: none; + color: var(--dark-text-color); + text-align: center; + font-size: 14px; } body.light-mode .output-box .filename { - color: var(--light-text-color); + color: var(--light-text-color); } body.dark-mode .output-box .filename { - color: var(--dark-text-color); + color: var(--dark-text-color); } /* Theme Toggle Switch ====================================================== */ .theme-switch { - position: relative; - display: inline-block; - width: 50px; - height: 25px; - margin-left: 10px; + position: relative; + display: inline-block; + width: 50px; + height: 25px; + margin-left: 10px; } /* Hide the default checkbox button */ .theme-switch input { - opacity: 0; - width: 0; - height: 0; + opacity: 0; + width: 0; + height: 0; } .theme-switch span { - position: absolute; - cursor: pointer; - background-color: var(--light-banner-hover); - border-radius: 25px; - top: 0; - left: 0; - right: 0; - bottom: 0; - transform: translateY(-30%); /* Center vertically */ - transition: 0.4s; + position: absolute; + cursor: pointer; + background-color: var(--light-banner-hover); + border-radius: 25px; + top: 0; + left: 0; + right: 0; + bottom: 0; + transform: translateY(-30%); /* Center vertically */ + transition: 0.4s; } .theme-switch span::before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 4px; - bottom: 3px; - background-color: var(--light-bg-color); - border-radius: 50%; - transition: none; /* Disable animation by default */ + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 3px; + background-color: var(--light-bg-color); + border-radius: 50%; + transition: none; /* Disable animation by default */ } .theme-switch span.animate::before { - transition: transform 0.4s ease, background-color 0.4s ease; + transition: transform 0.4s ease, background-color 0.4s ease; } .theme-switch input:checked + span { - background-color: var(--dark-bg-color); + background-color: var(--dark-bg-color); } .theme-switch input:checked + span::before { - transform: translateX(25px); - background-color: var(--dark-text-color); + transform: translateX(25px); + background-color: var(--dark-text-color); } /* Scrollbar ================================================================ */ @@ -987,8 +985,8 @@ body.no-transition .theme-switch span, body.no-transition button, body.no-transition #close-sidenav, -body.no-transition #home-button, -body.no-transition #back-button, +body.no-transition #home-button, +body.no-transition #back-button, body.no-transition #forward-button, body.no-transition #topmenu, body.no-transition .sidenav-category .folder > span, @@ -1003,129 +1001,129 @@ // Side Navigation functions =================================================== function openNav() { - const sidenav = document.getElementById("theSideNav"); - const main = document.getElementById("main"); + const sidenav = document.getElementById("theSideNav"); + const main = document.getElementById("main"); - sidenav.style.width = "360px"; - sidenav.style.paddingLeft = "20px"; - main.style.marginLeft = "360px"; - document.getElementById("open-sidenav").style.display = "none"; - document.getElementById("close-sidenav").style.display = "inline-block"; + sidenav.style.width = "360px"; + sidenav.style.paddingLeft = "20px"; + main.style.marginLeft = "360px"; + document.getElementById("open-sidenav").style.display = "none"; + document.getElementById("close-sidenav").style.display = "inline-block"; } function closeNav() { - const sidenav = document.getElementById("theSideNav"); - const main = document.getElementById("main"); + const sidenav = document.getElementById("theSideNav"); + const main = document.getElementById("main"); - sidenav.style.width = "0"; // Close the sidenav - sidenav.style.paddingLeft = "0"; // Reset padding - main.style.marginLeft = "0"; // Reset page content position - document.getElementById("open-sidenav").style.display = "inline-block"; - document.getElementById("close-sidenav").style.display = "none"; + sidenav.style.width = "0"; // Close the sidenav + sidenav.style.paddingLeft = "0"; // Reset padding + main.style.marginLeft = "0"; // Reset page content position + document.getElementById("open-sidenav").style.display = "inline-block"; + document.getElementById("close-sidenav").style.display = "none"; } function toggleFolder(folderHeader) { - const folder = folderHeader.parentElement; - const folderContents = folderHeader.nextElementSibling; + const folder = folderHeader.parentElement; + const folderContents = folderHeader.nextElementSibling; - if (folder.classList.contains("open")) { - folder.classList.remove("open"); - folderContents.style.display = "none"; - } else { - folder.classList.add("open"); - folderContents.style.display = "block"; - } + if (folder.classList.contains("open")) { + folder.classList.remove("open"); + folderContents.style.display = "none"; + } else { + folder.classList.add("open"); + folderContents.style.display = "block"; + } - // Save the updated state - saveSideNavState(); + // Save the updated state + saveSideNavState(); } function saveSideNavState() { - const sidenav = document.getElementById("theSideNav"); - const isOpen = sidenav.style.width !== "0px"; // Check if sidenav is open + const sidenav = document.getElementById("theSideNav"); + const isOpen = sidenav.style.width !== "0px"; // Check if sidenav is open - // Save the state of each folder - const folderStates = Array.from(document.querySelectorAll(".folder")).map(folder => ({ - id: folder.querySelector("span").textContent.trim(), // Use folder name as identifier - isOpen: folder.classList.contains("open") // Check if folder is open - })); + // Save the state of each folder + const folderStates = Array.from(document.querySelectorAll(".folder")).map(folder => ({ + id: folder.querySelector("span").textContent.trim(), // Use folder name as identifier + isOpen: folder.classList.contains("open") // Check if folder is open + })); - // Save the sidenav and folder states to localStorage - localStorage.setItem("sidenavState", JSON.stringify({ isOpen, folderStates })); + // Save the sidenav and folder states to localStorage + localStorage.setItem("sidenavState", JSON.stringify({ isOpen, folderStates })); } function restoresidenavState() { - const savedState = localStorage.getItem("sidenavState"); - if (savedState) { - const { isOpen, folderStates } = JSON.parse(savedState); - const sidenav = document.getElementById("theSideNav"); - const main = document.getElementById("main"); - - // Temporarily disable animations on page load so elements don't move - sidenav.style.transition = "none"; - main.style.transition = "none"; - const chevrons = document.querySelectorAll(".chevron"); - chevrons.forEach(chevron => { - chevron.style.transition = "none"; - }); - - // Restore side navigation state - if (isOpen) { - openNav() - } else { - closeNav() - } - - // Restore folder open/closed states - folderStates.forEach(({ id, isOpen }) => { - const folder = Array.from(document.querySelectorAll(".folder")) - .find(folder => folder.querySelector("span").textContent.trim() === id); - - if (folder) { - const folderContents = folder.querySelector(".folder-contents"); - if (isOpen) { - folder.classList.add("open"); - folderContents.style.display = "block"; - } else { - folder.classList.remove("open"); - folderContents.style.display = "none"; - } - } - }); - - // Re-enable animation - setTimeout(() => { - sidenav.style.transition = ""; - main.style.transition = ""; - chevrons.forEach(chevron => { - chevron.style.transition = ""; - }); - }, 100); - } + const savedState = localStorage.getItem("sidenavState"); + if (savedState) { + const { isOpen, folderStates } = JSON.parse(savedState); + const sidenav = document.getElementById("theSideNav"); + const main = document.getElementById("main"); + + // Temporarily disable animations on page load so elements don't move + sidenav.style.transition = "none"; + main.style.transition = "none"; + const chevrons = document.querySelectorAll(".chevron"); + chevrons.forEach(chevron => { + chevron.style.transition = "none"; + }); + + // Restore side navigation state + if (isOpen) { + openNav() + } else { + closeNav() + } + + // Restore folder open/closed states + folderStates.forEach(({ id, isOpen }) => { + const folder = Array.from(document.querySelectorAll(".folder")) + .find(folder => folder.querySelector("span").textContent.trim() === id); + + if (folder) { + const folderContents = folder.querySelector(".folder-contents"); + if (isOpen) { + folder.classList.add("open"); + folderContents.style.display = "block"; + } else { + folder.classList.remove("open"); + folderContents.style.display = "none"; + } + } + }); + + // Re-enable animation + setTimeout(() => { + sidenav.style.transition = ""; + main.style.transition = ""; + chevrons.forEach(chevron => { + chevron.style.transition = ""; + }); + }, 100); + } } function saveSidenavScrollPosition() { - const sidenav = document.getElementById("theSideNav"); - const scrollPosition = sidenav.scrollTop; - localStorage.setItem("sidenavScrollPosition", scrollPosition); + const sidenav = document.getElementById("theSideNav"); + const scrollPosition = sidenav.scrollTop; + localStorage.setItem("sidenavScrollPosition", scrollPosition); } function restoreSidenavScrollPosition() { - const sidenav = document.getElementById("theSideNav"); - const savedPosition = localStorage.getItem("sidenavScrollPosition"); - if (savedPosition !== null) { - sidenav.scrollTop = parseInt(savedPosition, 10); - } + const sidenav = document.getElementById("theSideNav"); + const savedPosition = localStorage.getItem("sidenavScrollPosition"); + if (savedPosition !== null) { + sidenav.scrollTop = parseInt(savedPosition, 10); + } } function adjustSidenavOffset() { - const topMenu = document.getElementById("topmenu"); - const sidenav = document.getElementById("theSideNav"); + const topMenu = document.getElementById("topmenu"); + const sidenav = document.getElementById("theSideNav"); - if (topMenu) { - const topMenuHeight = topMenu.offsetHeight; - document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); - } + if (topMenu) { + const topMenuHeight = topMenu.offsetHeight; + document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); + } } adjustSidenavOffset(); @@ -1146,8 +1144,8 @@ // Prevent flickering on page navigation window.addEventListener("beforeunload", () => { - saveSideNavState(); - saveSidenavScrollPosition(); + saveSideNavState(); + saveSidenavScrollPosition(); }); window.addEventListener("load", restoreSidenavScrollPosition); @@ -1308,6 +1306,7 @@ </script> """ + def _run_command(cmd): logger.debug('- Executing: '+GS.pasteable_cmd(cmd)) try: @@ -1351,6 +1350,7 @@ def __init__(self): self._expand_id = 'navigate' self._expand_ext = 'html' self._variant_name = self._find_variant_name() + def config(self, parent): super().config(parent) # Logo @@ -1812,8 +1812,10 @@ def generate_top_menu(self, category=''): # Left-aligned section (sidenav button, Back/Forward buttons, and category path) code += ' <td style="width: 33%;" align="left">\n' if self.nav_bar: - code += f' <span id="open-sidenav" style="font-size:{fsize};cursor:pointer" onclick="openNav()">☰</span>\n' - code += f' <span id="close-sidenav" style="font-size:{fsize};cursor:pointer;display:none;" onclick="closeNav()">⨉</span>\n' + code += (f' <span id="open-sidenav" style="font-size:{fsize};cursor:pointer" ' + f'onclick="openNav()">☰</span>\n') + code += (f' <span id="close-sidenav" style="font-size:{fsize};cursor:pointer;display:none;" ' + f'onclick="closeNav()">⨉</span>\n') code += f' <button id="back-button" onclick="history.back()" style="font-size:{fsize};">↩</button>\n' code += f' <button id="forward-button" onclick="history.forward()" style="font-size:{fsize};">↪</button>\n' @@ -1879,7 +1881,10 @@ def generate_top_menu(self, category=''): img_name = os.path.join('images', 'logo.png') if self.logo_url: code += f' <a href="{self.logo_url}" style="margin-right: 10px;">\n' - code += f' <img src="{img_name}" alt="Logo" style="max-height: {logo_height}; vertical-align: middle; display: inline-block; position: relative; top: -5px;">\n' + code += (f' <img src="{img_name}" alt="Logo" ' + f'style="max-height: {logo_height}; vertical-align: middle; display: inline-block; ' + f'position: relative; top: -5px;">\n') + if self.logo_url: code += ' </a>\n' @@ -1890,7 +1895,8 @@ def generate_top_menu(self, category=''): <span></span> </label> ''' - code += f' <button id="home-button" onclick="location.href=\'{self.home}\'" style="font-size:{fsize};">🏠︎</button>\n' + code += (f' <button id="home-button" onclick="location.href=\'{self.home}\'" ' + f'style="font-size:{fsize};">🏠︎</button>\n') code += ' </td>\n' code += ' </tr>\n' @@ -1918,7 +1924,8 @@ def solve_title(self): if self._git_command: res = '' try: - res = run_command([self._git_command, 'remote', 'get-url', 'origin'], just_raise=True) + res = run_command([self._git_command, 'remote', 'get-url', 'origin'], + just_raise=True) except subprocess.CalledProcessError: pass if res: From acf350b4a3e2521dfd65c1d657494741f42b9025 Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 11 Jan 2025 00:13:05 +0100 Subject: [PATCH 03/14] [Navigate Results Modern][Fixed] Underindentation --- kibot/out_navigate_results_modern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index 17e92bc68..0afca6d9f 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -1896,7 +1896,7 @@ def generate_top_menu(self, category=''): </label> ''' code += (f' <button id="home-button" onclick="location.href=\'{self.home}\'" ' - f'style="font-size:{fsize};">🏠︎</button>\n') + f'style="font-size:{fsize};">🏠︎</button>\n') code += ' </td>\n' code += ' </tr>\n' From a43710df93cfe7b8b7471c0ddde74cedaab13cdc Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 11 Jan 2025 00:49:23 +0100 Subject: [PATCH 04/14] [Nav Results Modern][Fixed] Output virtual box below category before category boxes were not clickable if too close to output titles --- kibot/out_navigate_results_modern.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index 0afca6d9f..e2627bf5d 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -489,6 +489,7 @@ /* Category boxes (folder) -------------------------------------------------- */ .category-box { + z-index: 1000; display: flex; align-items: center; justify-content: center; @@ -552,6 +553,7 @@ /* Output boxes (files) ----------------------------------------------------- */ .output-box { + z-index: 999; background-color: var(--dark-bg-color-banner); border: 1px solid var(--dark-bg-color-banner); border-radius: 8px; @@ -571,6 +573,11 @@ position: relative; padding-top: var(--top-menu-height, 80px); margin-top: calc(-1 * var(--top-menu-height, 80px)); + pointer-events: none; /* Make it non-interactive */ +} + +.output-virtual-box > * { + pointer-events: auto; /* Allow its children to remain interactive */ } /* Some files (e.g. PDF, PNG) have wider output boxes */ From 6a8dc2c275bdf5ddd5d2d6e09f0de56b4b5044b9 Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 11 Jan 2025 08:55:47 +0100 Subject: [PATCH 05/14] [Nav Res Modern][Fixed] Disable transition on page load For the search bar to avoid flickering --- kibot/out_navigate_results_modern.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index e2627bf5d..f5dcd1d02 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -997,7 +997,8 @@ body.no-transition #forward-button, body.no-transition #topmenu, body.no-transition .sidenav-category .folder > span, -body.no-transition .category-box { +body.no-transition .category-box, +#search-bar { transition: none !important; /* Disable transition during page load */ } From 21c7f651eb46909ef5854fcc6a2d16d823b79e0a Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sun, 12 Jan 2025 11:52:47 +0100 Subject: [PATCH 06/14] [Nav Res Modern][Removed] Duplicate code for search bar --- kibot/out_navigate_results_modern.py | 194 ++++++++++----------------- 1 file changed, 70 insertions(+), 124 deletions(-) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index f5dcd1d02..05036e103 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -1158,6 +1158,75 @@ window.addEventListener("load", restoreSidenavScrollPosition); document.addEventListener("DOMContentLoaded", restoresidenavState); + +function initializeSearchBar(searchContainerId, outputSelector) { + const searchContainer = document.getElementById(searchContainerId); + if (!searchContainer) return; // Exit if container is not found + + const searchBar = searchContainer.querySelector("#search-bar"); + const autocompleteList = searchContainer.querySelector("#autocomplete-list"); + const outputLinks = document.querySelectorAll(outputSelector); + + // Collect output names and their hrefs + const outputs = Array.from(outputLinks).map(link => ({ + name: link.textContent.trim(), + href: link.getAttribute("href") + })); + + // Adjust autocomplete list width to match search bar width + function adjustAutocompleteWidth() { + const searchBarWidth = searchBar.offsetWidth; // Get search bar width + autocompleteList.style.width = `${searchBarWidth}px`; // Match autocomplete list width + } + + // Initial width adjustment + adjustAutocompleteWidth(); + + // Update width on window resize to handle responsiveness + window.addEventListener("resize", adjustAutocompleteWidth); + + // Update autocomplete suggestions + function updateAutocomplete(query) { + autocompleteList.innerHTML = ""; // Clear suggestions + + const matches = outputs.filter(output => + output.name.toLowerCase().includes(query.toLowerCase()) + ); + + matches.forEach(match => { + const listItem = document.createElement("li"); + listItem.textContent = match.name; + listItem.addEventListener("click", () => { + window.location.href = match.href; + }); + autocompleteList.appendChild(listItem); + }); + + autocompleteList.style.display = matches.length ? "block" : "none"; + } + + // Add event listeners for search bar input + searchBar.addEventListener("input", () => { + const query = searchBar.value.trim(); + if (query) updateAutocomplete(query); + else { + autocompleteList.innerHTML = ""; + autocompleteList.style.display = "none"; + } + }); + + // Hide suggestions when clicking outside + document.addEventListener("click", (event) => { + if (!searchBar.contains(event.target) && !autocompleteList.contains(event.target)) { + autocompleteList.style.display = "none"; + } + }); +} + +// Initialize search functionality +document.addEventListener("DOMContentLoaded", () => { + initializeSearchBar("search-container", ".sidenav-output"); +}); </script> """ @@ -1244,73 +1313,6 @@ // Apply the adjustment on page load and window resize window.addEventListener("DOMContentLoaded", adjustMainBodyOffset); window.addEventListener("resize", adjustMainBodyOffset); - -document.addEventListener("DOMContentLoaded", function () { - const searchBar = document.getElementById("search-bar"); - const autocompleteList = document.getElementById("autocomplete-list"); - const outputLinks = document.querySelectorAll(".sidenav-output"); - - // Collect output names and their hrefs - const outputs = Array.from(outputLinks).map(link => ({ - name: link.textContent.trim(), - href: link.getAttribute("href") - })); - - function updateAutocompleteWidth() { - const searchBarWidth = searchBar.offsetWidth; // Get the width of the search bar - autocompleteList.style.width = `${searchBarWidth}px`; // Match autocomplete width to search bar - } - - // Initial width adjustment - updateAutocompleteWidth(); - - // Update width on window resize to handle responsiveness - window.addEventListener("resize", updateAutocompleteWidth); - - - // Update autocomplete suggestions - function updateAutocomplete(query) { - // Clear previous suggestions - autocompleteList.innerHTML = ""; - - // Filter and display matching outputs - const matches = outputs.filter(output => - output.name.toLowerCase().includes(query.toLowerCase()) - ); - - if (matches.length > 0) { - matches.forEach(match => { - const listItem = document.createElement("li"); - listItem.textContent = match.name; - listItem.addEventListener("click", () => { - window.location.href = match.href; // Redirect to the selected output - }); - autocompleteList.appendChild(listItem); - }); - autocompleteList.style.display = "block"; // Show the list - } else { - autocompleteList.style.display = "none"; // Hide the list if no matches - } - } - - // Add event listeners to the search bar - searchBar.addEventListener("input", () => { - const query = searchBar.value.trim(); - if (query) { - updateAutocomplete(query); - } else { - autocompleteList.innerHTML = ""; // Clear suggestions - autocompleteList.style.display = "none"; // Hide the list - } - }); - - // Hide autocomplete list when clicking outside - document.addEventListener("click", (event) => { - if (!searchBar.contains(event.target) && !autocompleteList.contains(event.target)) { - autocompleteList.style.display = "none"; - } - }); -}); </script> """ @@ -1737,7 +1739,7 @@ def generate_sidenav(self, node, name): name, ext = os.path.splitext(name) code = ''' <div id="theSideNav" class="sidenav"> - <!-- Search bar with autocomplete --> + <!-- Search bar container --> <div id="search-container"> <input type="text" id="search-bar" placeholder="Search outputs..." autocomplete="off"> <ul id="autocomplete-list"></ul> @@ -1745,62 +1747,6 @@ def generate_sidenav(self, node, name): ''' code += self.generate_sidenav_one(node, 0, name, ext) code += '</div>\n' - code += """ - <script> - document.addEventListener("DOMContentLoaded", function () { - const searchBar = document.getElementById("search-bar"); - const autocompleteList = document.getElementById("autocomplete-list"); - const outputLinks = document.querySelectorAll(".sidenav-output"); - - // Collect output names and their hrefs - const outputs = Array.from(outputLinks).map(link => ({ - name: link.textContent.trim(), - href: link.getAttribute("href") - })); - - // Update autocomplete suggestions - function updateAutocomplete(query) { - // Clear previous suggestions - autocompleteList.innerHTML = ""; - - // Filter and display matching outputs - const matches = outputs.filter(output => - output.name.toLowerCase().includes(query.toLowerCase()) - ); - - matches.forEach(match => { - const listItem = document.createElement("li"); - listItem.textContent = match.name; - listItem.addEventListener("click", () => { - window.location.href = match.href; // Redirect to the selected output - }); - autocompleteList.appendChild(listItem); - }); - - // Hide the list if there are no matches - autocompleteList.style.display = matches.length ? "block" : "none"; - } - - // Add event listeners to the search bar - searchBar.addEventListener("input", () => { - const query = searchBar.value.trim(); - if (query) { - updateAutocomplete(query); - } else { - autocompleteList.innerHTML = ""; // Clear suggestions - autocompleteList.style.display = "none"; - } - }); - - // Hide autocomplete list when clicking outside - document.addEventListener("click", (event) => { - if (!searchBar.contains(event.target) && !autocompleteList.contains(event.target)) { - autocompleteList.style.display = "none"; - } - }); - }); - </script> - """ return code def generate_top_menu(self, category=''): From 091267d931e892dc3783989f04ff135689f340a5 Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sun, 12 Jan 2025 12:24:52 +0100 Subject: [PATCH 07/14] [Nav Res Modern][Added] Arrow navigation in search bar --- kibot/out_navigate_results_modern.py | 63 ++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index 05036e103..50409847e 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -984,6 +984,15 @@ color: var(--dark-hover-color); } +.highlighted { + background-color: var(--light-banner-hover); /* Same as hover background */ + color: var(--light-hover-color); /* Same as hover text color */ +} + +body.dark-mode .highlighted { + background-color: var(--dark-banner-hover); /* Same as hover background */ + color: var(--dark-hover-color); /* Same as hover text color */ +} /* New classes to remove transitions on page load =========================== */ @@ -1170,24 +1179,19 @@ // Collect output names and their hrefs const outputs = Array.from(outputLinks).map(link => ({ name: link.textContent.trim(), - href: link.getAttribute("href") + href: link.getAttribute("href"), })); - // Adjust autocomplete list width to match search bar width + let highlightedIndex = -1; // Index of the currently highlighted item + function adjustAutocompleteWidth() { - const searchBarWidth = searchBar.offsetWidth; // Get search bar width - autocompleteList.style.width = `${searchBarWidth}px`; // Match autocomplete list width + const searchBarWidth = searchBar.offsetWidth; + autocompleteList.style.width = `${searchBarWidth}px`; } - // Initial width adjustment - adjustAutocompleteWidth(); - - // Update width on window resize to handle responsiveness - window.addEventListener("resize", adjustAutocompleteWidth); - - // Update autocomplete suggestions function updateAutocomplete(query) { autocompleteList.innerHTML = ""; // Clear suggestions + highlightedIndex = -1; // Reset highlighting const matches = outputs.filter(output => output.name.toLowerCase().includes(query.toLowerCase()) @@ -1205,7 +1209,19 @@ autocompleteList.style.display = matches.length ? "block" : "none"; } - // Add event listeners for search bar input + function highlightItem(index) { + const items = autocompleteList.querySelectorAll("li"); + items.forEach((item, i) => { + if (i === index) { + item.classList.add("highlighted"); + item.scrollIntoView({ block: "nearest" }); + } else { + item.classList.remove("highlighted"); + } + }); + } + + // Add event listeners for search bar input and keydown searchBar.addEventListener("input", () => { const query = searchBar.value.trim(); if (query) updateAutocomplete(query); @@ -1215,18 +1231,41 @@ } }); + searchBar.addEventListener("keydown", (event) => { + const items = autocompleteList.querySelectorAll("li"); + if (!items.length) return; + + if (event.key === "ArrowDown") { + event.preventDefault(); + highlightedIndex = (highlightedIndex + 1) % items.length; + highlightItem(highlightedIndex); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + highlightedIndex = (highlightedIndex - 1 + items.length) % items.length; + highlightItem(highlightedIndex); + } else if (event.key === "Enter" && highlightedIndex >= 0) { + event.preventDefault(); + items[highlightedIndex].click(); + } + }); + // Hide suggestions when clicking outside document.addEventListener("click", (event) => { if (!searchBar.contains(event.target) && !autocompleteList.contains(event.target)) { autocompleteList.style.display = "none"; } }); + + // Adjust width on window resize + adjustAutocompleteWidth(); + window.addEventListener("resize", adjustAutocompleteWidth); } // Initialize search functionality document.addEventListener("DOMContentLoaded", () => { initializeSearchBar("search-container", ".sidenav-output"); }); + </script> """ From d928453b95561f744fdfe44ec96c77efde4c75a3 Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Thu, 16 Jan 2025 22:24:49 +0100 Subject: [PATCH 08/14] [Nav Res Modern][Changed] Various changes Icon and text layout in the top menu Clickable paths for arbitrary ancestors Up arrow to go back in history Arbitrary logo size Atomic inclusion of markdown-it.js --- kibot/out_navigate_results_modern.py | 367 ++++++++++++++++++--------- 1 file changed, 251 insertions(+), 116 deletions(-) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index 50409847e..776a3cbcc 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -26,8 +26,9 @@ import os import subprocess import pprint +import re from shutil import copy2 -from .bom.kibot_logo import KIBOT_LOGO +from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H from .error import KiPlotConfigurationError from .gs import GS from .optionable import Optionable, BaseOptions @@ -36,9 +37,22 @@ from .pre_base import BasePreFlight from .registrable import RegOutput from .macros import macros, document, output_class # noqa: F401 -from . import log +from . import log, __version__ logger = log.get_logger() +CAT_IMAGE = {'PCB': 'pcbnew', + 'Schematic': 'eeschema', + 'Compress': 'zip', + 'fabrication': 'fabrication', + 'export': 'export', + 'assembly': 'assembly_simple', + 'repair': 'repair', + 'docs': 'project', + 'BoM': 'bom', + '3D': '3d', + 'gerber': 'gerber', + 'drill': 'load_drill', + 'Auxiliar': 'repair'} EXT_IMAGE = {'gbr': 'file_gbr', 'gtl': 'file_gbr', 'gtp': 'file_gbr', @@ -92,7 +106,8 @@ EXT_IMAGE['gl'+n] = 'file_gbr' EXT_IMAGE['g'+n] = 'file_gbr' EXT_IMAGE['gp'+n] = 'file_gbr' - +CAT_REP = {'PCB': ['pdf_pcb_print', 'svg_pcb_print', 'pcb_print'], + 'Schematic': ['pdf_sch_print', 'svg_sch_print']} BIG_ICON = 512 MID_ICON = 64 @@ -185,6 +200,7 @@ color: var(--dark-text-color); cursor: pointer; transition: color 0.3s ease; + user-select: none; } body.light-mode #topmenu button, @@ -210,8 +226,7 @@ line-height: 36px; text-align: center; font-size: 28px; - margin-left: 10px; - user-select: none; /* Prevent text selection */ + margin-left: 15px; } /* Hover effects */ @@ -269,6 +284,7 @@ background-color 0.2s ease-in-out; box-sizing: border-box; padding-top: 0; + padding-bottom: 40px; } @@ -486,6 +502,38 @@ color: var(--dark-text-color); } +/* Kibot version ------------------------------------------------------------ */ + +.generator { + text-align: right; + font-size: 0.6em; + text-decoration: none; +} + +.generator a { + text-decoration: none; /* Removes the underline */ +} + +/* Dark Mode: Regular Text */ +body.dark-mode .generator { + color: var(--dark-text-color-accent); +} + +/* Dark Mode: Hyperlinks */ +body.dark-mode .generator a { + color: var(--dark-hover-color); /* Hyperlink */ +} + +/* Light Mode: Regular Text */ +body.light-mode .generator { + color: var(--light-text-color-accent); +} + +/* Light Mode: Hyperlinks */ +body.light-mode .generator a { + color: var(--light-hover-color); /* Hyperlink */ +} + /* Category boxes (folder) -------------------------------------------------- */ .category-box { @@ -693,7 +741,6 @@ left: 0; right: 0; bottom: 0; - transform: translateY(-30%); /* Center vertically */ transition: 0.4s; } @@ -780,10 +827,9 @@ .markdown-content { font-family: Roboto, sans-serif; line-height: 1.6; - margin-left: 150px; - margin-right: 150px; padding: 15px; border-radius: 5px; + max-width: calc(100% - 180px); white-space: pre-wrap; /* Handle preformatted text */ transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; } @@ -899,6 +945,31 @@ transition: opacity 0.4s ease; } +.markdown-content pre::-webkit-scrollbar { + height: 12px; /* Horizontal scrollbar height */ +} + +.markdown-content pre::-webkit-scrollbar-thumb { + background: var(--dark-banner-hover); /* Match other scrollbar thumb color */ + border-radius: 6px; /* Round edges */ + border: 2px solid var(--dark-bg-color); /* Outer border matches background */ +} + +.markdown-content pre::-webkit-scrollbar-track { + background: var(--dark-bg-color); /* Match the background color */ + border-radius: 6px; +} + +body.light-mode .markdown-content pre::-webkit-scrollbar-thumb { + background: var(--light-banner-hover); /* Light mode thumb color */ + border: 2px solid var(--light-bg-color); /* Light mode border */ +} + +body.light-mode .markdown-content pre::-webkit-scrollbar-track { + background: var(--light-bg-color); /* Light mode track background */ +} + + /* Search bar =============================================================== */ #search-container, @@ -1006,6 +1077,7 @@ body.no-transition #forward-button, body.no-transition #topmenu, body.no-transition .sidenav-category .folder > span, +body.no-transition .sidenav-output, body.no-transition .category-box, #search-bar { transition: none !important; /* Disable transition during page load */ @@ -1324,6 +1396,24 @@ }, 50); // Allow rendering to complete before enabling transitions }); +function adjustMainBodyOffset() { + const topMenu = document.getElementById("topmenu"); + const mainBody = document.getElementById("main"); + + if (topMenu && mainBody) { + const topMenuHeight = topMenu.offsetHeight; + mainBody.style.marginTop = `${topMenuHeight}px`; + } +} + +// Apply the adjustment on page load and window resize +window.addEventListener("DOMContentLoaded", adjustMainBodyOffset); +window.addEventListener("resize", adjustMainBodyOffset); +</script> +""" + +SCRIPT_MARKDOWN = """ +<script> document.addEventListener('DOMContentLoaded', function () { const md = window.markdownit({ html: true, @@ -1338,20 +1428,6 @@ container.innerHTML = md.render(rawMarkdown); }); }); - -function adjustMainBodyOffset() { - const topMenu = document.getElementById("topmenu"); - const mainBody = document.getElementById("main"); - - if (topMenu && mainBody) { - const topMenuHeight = topMenu.offsetHeight; - mainBody.style.marginTop = `${topMenuHeight}px`; - } -} - -// Apply the adjustment on page load and window resize -window.addEventListener("DOMContentLoaded", adjustMainBodyOffset); -window.addEventListener("resize", adjustMainBodyOffset); </script> """ @@ -1384,6 +1460,8 @@ def __init__(self): The KiBot logo is used by default """ self.logo_url = 'https://github.com/INTI-CMNB/KiBot/' """ Target link when clicking the logo """ + self.logo_force_height = -1 + """ Force logo height in px. Useful to get consistent heights across different logos. """ self.title = '' """ Title for the page, when empty KiBot will try using the schematic or PCB title. If they are empty the name of the project, schematic or PCB file is used. @@ -1395,6 +1473,8 @@ def __init__(self): """ Add a side navigation bar to quickly access to the outputs """ self.render_markdown = True """ If True, markdown files are rendered; otherwise, they are treated like other files """ + self.display_kibot_version = True + """ If True, display the KiBot version at the bottom of each page """ super().__init__() self._expand_id = 'navigate' self._expand_ext = 'html' @@ -1410,21 +1490,26 @@ def config(self, parent): if not os.path.isfile(self.logo): raise KiPlotConfigurationError('Missing logo file `{}`'.format(self.logo)) try: - self._logo_data, _, _, _ = read_png(self.logo, logger) + self._logo_data, self._logo_w, self._logo_h, _ = read_png(self.logo, logger) except TypeError as e: raise KiPlotConfigurationError(f'Only PNG images are supported for the logo ({e})') if self.logo == '': # Internal logo + self._logo_w = int(KIBOT_LOGO_W/4) + self._logo_h = int(KIBOT_LOGO_H/4) self._logo_data = base64.b64decode(KIBOT_LOGO) elif self.logo is None: + self._logo_w = self._logo_h = 0 self._logo_data = '' + if self.logo_force_height > 0: + self._logo_w = self.logo_force_height/self._logo_h*self._logo_w + self._logo_h = self.logo_force_height # Title URL if isinstance(self.title_url, bool): self.title_url = '' if self.title_url else None def add_to_tree(self, cat, out, o_tree): - # Add `out` to `o_tree` in the `cat` category - if cat == '.': + if cat == '.' or cat == './': # Place output directly at the root level o_tree[out.name] = out else: @@ -1472,6 +1557,46 @@ def can_be_converted(self, ext): return False return ext in IMAGEABLES_SVG or ext in IMAGEABLES_GS or ext in IMAGEABLES_SIMPLE + def get_image_for_cat(self, cat): + img = None + if cat in CAT_REP and self.convert_command is not None: + outs_rep = CAT_REP[cat] + rep_file = None + # Look in all outputs + for o in RegOutput.get_outputs(): + if o.type in outs_rep: + out_dir = get_output_dir(o.dir, o, dry=True) + targets = o.get_targets(out_dir) + for tg in targets: + ext = os.path.splitext(tg)[1][1:].lower() + if os.path.isfile(tg) and self.can_be_converted(ext): + rep_file = tg + break + if rep_file: + break + if rep_file: + cat_img, _ = self.get_image_for_file(rep_file, cat, no_icon=True) + cat_img = (f''' + <div class="category-title"> + <img src="{cat_img}" alt="{cat}" width="{int(BIG_ICON*0.6)}" height="{int(BIG_ICON*0.6)}"> + <br><span class="category-title">{cat}</span> + </div> + ''') + return cat_img + + if cat in CAT_IMAGE: + img = self.copy(CAT_IMAGE[cat], BIG_ICON) + # Include the category name with the category-title class + cat_img = ( + f'<div class="category-title">' + f' <img src="{img}" alt="{cat}" width="{int(BIG_ICON*0.6)}" height="{int(BIG_ICON*0.6)}">' + f' <br><span class="category-title">{cat}</span>' + f'</div>' + ) + return cat_img + + return f'<div class="category-title">{cat}</div>' # Fallback if no image + def compose_image(self, file, ext, img, out_name, no_icon=False): if not os.path.isfile(file): logger.warning(W_NOTYET+"{} not yet generated, using an icon".format(os.path.relpath(file))) @@ -1544,9 +1669,10 @@ def get_image_for_file(self, file, out_name, no_icon=False, image=None): format(ext_img, 'td-normal' if no_icon else 'td-small', out_name if no_icon else file)) return file, wide - def add_nav_bar(self, f, prev): - if self.nav_bar: - f.write(SCRIPT_NAV_BAR) + def write_kibot_version(self, f): + if self.display_kibot_version: + f.write('<p class="generator">Generated by <a href="https://github.com/INTI-CMNB/KiBot/">KiBot</a> v{}</p>\n'. + format(__version__)) def write_head(self, f, title): f.write('<!DOCTYPE html>\n') @@ -1557,7 +1683,8 @@ def write_head(self, f, title): f.write(' <link rel="stylesheet" href="styles.css">\n') f.write(' <link rel="icon" href="favicon.ico">\n') # Include Markdown-it - f.write(' <script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>\n') + if self.render_markdown: + f.write(' <script src="markdown-it.min.js"></script>\n') f.write('</head>\n') f.write('<body>\n') f.write(self.sidenav) @@ -1570,8 +1697,7 @@ def generate_cat_page_for(self, name, node, prev, category): self.write_head(f, category) name, ext = os.path.splitext(name) - # Start a vertically aligned container for categories - f.write('<div class="categories-container">\n') + f.write('<div>\n') for cat, content in node.items(): if not isinstance(content, dict): @@ -1587,28 +1713,19 @@ def generate_cat_page_for(self, name, node, prev, category): </a> ''') - # Close the container f.write('</div>\n') # Generate outputs below the categories self.generate_outputs(f, node) - self.add_nav_bar(f, prev) + if self.nav_bar: + f.write(SCRIPT_NAV_BAR) + if self.render_markdown: + f.write(SCRIPT_MARKDOWN) f.write(SCRIPT) + self.write_kibot_version(f) f.write('</body>\n</html>\n') def adjust_image_paths(self, md_content, current_dir, html_output_dir): - """ - Adjusts image paths in markdown content to be relative to the HTML output directory. - - Args: - md_content (str): Raw markdown content. - current_dir (str): Directory of the markdown file. - html_output_dir (str): Absolute directory where the HTML file is generated. - - Returns: - str: Updated markdown content with adjusted image paths. - """ - import re image_pattern = r'!\[.*?\]\((.*?)\)' # Markdown image paths: ![Alt text](path/to/image.png) html_img_pattern = r'<img\s+[^>]*src="([^"]+)"' # HTML img src="path/to/image.png" @@ -1693,8 +1810,12 @@ def generate_end_page_for(self, name, node, prev, category): self.write_head(f, category) name, ext = os.path.splitext(name) self.generate_outputs(f, node) - self.add_nav_bar(f, prev) + if self.nav_bar: + f.write(SCRIPT_NAV_BAR) + if self.render_markdown: + f.write(SCRIPT_MARKDOWN) f.write(SCRIPT) + self.write_kibot_version(f) f.write('</body>\n</html>\n') def generate_page_for(self, node, name, prev=None, category=''): @@ -1733,6 +1854,15 @@ def get_html_names(self, node, name, files, prev=None, category=''): else: files.append(os.path.join(self.out_dir, name)) + def get_html_names_for_path(self, category_path, name, ext): + files = [] + node = self.create_tree() + self.get_html_names_cat(name, node, None, category_path, files) + for file_path in files: + if category_path.replace('/', '_') in file_path: + return os.path.basename(file_path) + ext + return None + def create_tree(self): o_tree = {} BasePreFlight.configure_all() @@ -1790,110 +1920,114 @@ def generate_sidenav(self, node, name): def generate_top_menu(self, category=''): """ - Generates the top menu with Back, Forward, Home, sidenav buttons, and a logo. - Displays the current folder name dynamically next to the forward arrow with wrapping for '/'. - Adds revision, variant, and company information. + Generates the top menu with clickable category paths and an up arrow button + that navigates to the root page if the current path is the category name. """ fsize = f'{TITLE_HEIGHT}px' - logo_height = f'{TITLE_HEIGHT}px' - small_font_size = f'{int(TITLE_HEIGHT) - 8}px' # Smaller font for revision/variant and company + fsize_up = f'{TITLE_HEIGHT+12}px' + small_font_size = f'{int(TITLE_HEIGHT) - 12}px' smallest_font_size = f'{int(TITLE_HEIGHT) - 16}px' - code = '<div id="topmenu" class="topmenu">\n' - code += ' <table style="width:100%; table-layout: fixed; height:100%;">\n' - code += ' <tr style="vertical-align: middle;">\n' - - # Left-aligned section (sidenav button, Back/Forward buttons, and category path) - code += ' <td style="width: 33%;" align="left">\n' + + code = ''' + <div id="topmenu" class="topmenu"> + ''' + + # Left Section (Buttons + Path) + code += '<div style="display: flex; align-items: center; flex: 1; min-width: 0; gap: 10px;">\n' if self.nav_bar: - code += (f' <span id="open-sidenav" style="font-size:{fsize};cursor:pointer" ' - f'onclick="openNav()">☰</span>\n') - code += (f' <span id="close-sidenav" style="font-size:{fsize};cursor:pointer;display:none;" ' - f'onclick="closeNav()">⨉</span>\n') - code += f' <button id="back-button" onclick="history.back()" style="font-size:{fsize};">↩</button>\n' - code += f' <button id="forward-button" onclick="history.forward()" style="font-size:{fsize};">↪</button>\n' - - # Display the current folder name (category path) - if category: # Display the current folder name + code += (f'<span id="open-sidenav" style="font-size:{fsize};cursor:pointer;" ' + f'onclick="openNav()">☰</span>\n') + code += (f'<span id="close-sidenav" style="font-size:{fsize};cursor:pointer;display:none;" ' + f'onclick="closeNav()">⨉</span>\n') + code += f'<button id="back-button" onclick="history.back()" style="font-size:{fsize};">↩</button>\n' + code += f'<button id="forward-button" onclick="history.forward()" style="font-size:{fsize};">↪</button>\n' + + # Up and Home Buttons + parent_path = '/'.join(category.strip('/').split('/')[:-1]) if category else None + if parent_path is not None: + target_file = self.get_html_names_for_path(parent_path, *os.path.splitext(self.home)) if parent_path else self.home + code += (f'<button id="up-button" onclick="location.href=\'{target_file}\'" ' + f'style="font-size:{fsize_up}; position: relative; top: -3px;">⌅</button>\n') + else: + code += (f'<button id="up-button" disabled style="font-size:{fsize_up}; color: gray; cursor: not-allowed; position: relative; top: -3px;">⌅</button>\n') + + code += f'<button id="home-button" onclick="location.href=\'{self.home}\'" style="font-size:{TITLE_HEIGHT-5}px; position: relative; top: -2px;">🏠︎</button>\n' + + # Category Path + if category: + path_parts = category.lstrip('/').split('/') + path_links = [] + current_path = '' + name, ext = os.path.splitext(self.home) + for part in path_parts: + current_path += f'{part}/' + html_file_name = self.get_html_names_for_path(current_path.rstrip('/'), name, ext) + path_links.append(f'<a href="{html_file_name}" style="text-decoration:none;color:inherit;">{part}</a>') + clickable_path = '/<wbr>'.join(path_links) code += f''' <span style=" font-size:{small_font_size}; - margin-left: 10px; - color: var(--text-color-accent); /* Use dynamic variable */ - position: relative; - top: -5px; /* Add -5px vertical offset */ - white-space: normal; /* Allow wrapping */ - word-break: keep-all; /* Prevent breaking at arbitrary places */ - overflow-wrap: normal; /* Allow wrapping only where explicitly defined */ + color: var(--text-color-accent); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; "> - {category.lstrip('/').replace('/', '/<wbr>')} - </span> - ''' - code += ' </td>\n' + {clickable_path} + </span>\n''' + + code += '</div>\n' - # Centered title with company below - code += ' <td style="width: 33%;" align="center">\n' + # Center Section (Title + Company Name) + code += '<div style="text-align: center; flex: 1; min-width: 200px;">\n' if self._solved_title: if self.title_url: - code += f' <a href="{self.title_url}" style="text-decoration: none; color: inherit;">\n' - code += f' <span style="font-size:{fsize};">{self._solved_title}</span>\n' + code += f'<a href="{self.title_url}" style="text-decoration: none; color: inherit;">\n' + code += f'<span style="font-size:{fsize};">{self._solved_title}</span>\n' if self.title_url: - code += ' </a>\n' - # Add company information + code += '</a>\n' code += f''' <div style=" font-size:{smallest_font_size}; color: var(--text-color-accent); - text-align: center; margin-top: 5px;"> {self._solved_company} </div> ''' - code += ' </td>\n' - - # Right-aligned section (Logo, Revision/Variant, Toggle, and Home button) - code += ' <td style="width: 33%;" align="right">\n' + code += '</div>\n' - # Revision and Variant + # Right Section (Logo, Revision/Variant, Theme Toggle) + code += '<div style="display: flex; align-items: center; flex: 1; justify-content: flex-end; min-width: 0; gap: 10px; padding-right: 10px;">\n' code += f''' <div style=" - display: inline-block; text-align: left; - position: relative; - top: 5px; /* Add 5px vertical offset */ font-size:{smallest_font_size}; - color: var(--text-color-accent); - margin-right: 20px; /* Space to the left of the logo */ - "> + margin-right: 10px; + color: var(--text-color-accent);"> <div style="margin-bottom: 5px;">Rev. {self._solved_revision}</div> - Variant: {self._variant_name} - </div> - ''' - - # Add the logo + <div>Variant: {self._variant_name}</div> + </div>\n''' if self.logo is not None: img_name = os.path.join('images', 'logo.png') if self.logo_url: - code += f' <a href="{self.logo_url}" style="margin-right: 10px;">\n' - code += (f' <img src="{img_name}" alt="Logo" ' - f'style="max-height: {logo_height}; vertical-align: middle; display: inline-block; ' - f'position: relative; top: -5px;">\n') - + code += f'<a href="{self.logo_url}" style="margin-right: 10px;">\n' + code += (f'<img src="{img_name}" alt="Logo" ' + f'style="max-height: {str(self._logo_h)}px; max-width: {str(self._logo_w)}px;">\n') if self.logo_url: - code += ' </a>\n' + code += '</a>\n' - # Add the toggle + # Move the Theme Toggle Left code += ''' - <label class="theme-switch"> + <label class="theme-switch" style=" + position: relative; + margin-right: 10px; /* Move toggle slightly left */ + max-width: 100%; /* Prevent overflow */ + "> <input type="checkbox" id="themeToggle" onchange="toggleTheme()"> <span></span> </label> ''' - code += (f' <button id="home-button" onclick="location.href=\'{self.home}\'" ' - f'style="font-size:{fsize};">🏠︎</button>\n') - code += ' </td>\n' - - code += ' </tr>\n' - code += ' </table>\n' + code += '</div>\n' code += '</div>\n' return code @@ -1953,6 +2087,7 @@ def solve_company(self): def run(self, name): self.out_dir = os.path.dirname(name) self.img_src_dir = GS.get_resource_path('images') + self.js_src_dir = GS.get_resource_path('navigate_results') self.img_dst_dir = os.path.join(self.out_dir, 'images') os.makedirs(self.img_dst_dir, exist_ok=True) self.copied_images = {} @@ -1971,9 +2106,9 @@ def run(self, name): self.ps2img_avail = self.check_tool('Ghostscript') # Create the pages self.home = name - self.back_img = self.copy('back', MID_ICON) - self.home_img = self.copy('home', MID_ICON) copy2(os.path.join(self.img_src_dir, 'favicon.ico'), os.path.join(self.out_dir, 'favicon.ico')) + if self.render_markdown: + copy2(os.path.join(self.js_src_dir, 'markdown-it.min.js'), os.path.join(self.out_dir, 'markdown-it.min.js')) # Copy the logo image if self.logo is not None: with open(os.path.join(self.out_dir, 'images', 'logo.png'), 'wb') as f: From 5badb5f214e5bd824800f5c143cf7f2b1f434e62 Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 18 Jan 2025 11:58:51 +0100 Subject: [PATCH 09/14] [Nav Res Modern][Added] Category images option --- kibot/out_navigate_results_modern.py | 157 ++++++++++++++++----------- 1 file changed, 91 insertions(+), 66 deletions(-) diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py index 776a3cbcc..acc3223df 100644 --- a/kibot/out_navigate_results_modern.py +++ b/kibot/out_navigate_results_modern.py @@ -538,18 +538,19 @@ .category-box { z-index: 1000; - display: flex; - align-items: center; - justify-content: center; background-color: var(--dark-bg-color-banner); border: 1px solid var(--dark-bg-color-banner); border-radius: 8px; - padding: 10px 20px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 400px; + height: 140; text-decoration: none; - color: var(--light-text-color); - max-width: 400px; transition: background-color 0.3s ease, transform 0.2s ease; - margin: 10px auto; + margin-bottom: 0px; } body.light-mode .category-box { @@ -565,6 +566,14 @@ border: var(--dark-bg-color-banner); } +.category-box img { + margin-top: 10px; + max-width: 100%; + max-height: 100%; + height: auto; + margin-bottom: 10px; +} + .category-title { font-size: 1.4em; font-weight: 500; @@ -572,6 +581,8 @@ color: #e5e5e5; text-decoration: none; display: inline-block; + margin-top: 0px; + margin-bottom: 0px; } body.light-mode .category-title { @@ -636,10 +647,11 @@ } .output-box img { + margin-top: 10px; max-width: 100%; max-height: 100%; height: auto; - margin-bottom: 10px; + margin-bottom: 0px; } /* The output boxes are centered and wrap around */ @@ -685,7 +697,7 @@ .output-box .output-name { color: #8997c6; font-size: 14px; - margin-top: 8px; + margin-top: 0px; text-align: center; } @@ -704,6 +716,7 @@ color: var(--dark-text-color); text-align: center; font-size: 14px; + margin-bottom: 7px; } body.light-mode .output-box .filename { @@ -1475,6 +1488,8 @@ def __init__(self): """ If True, markdown files are rendered; otherwise, they are treated like other files """ self.display_kibot_version = True """ If True, display the KiBot version at the bottom of each page """ + self.display_category_images = True + """ If True, we try to display images for categories according to the category type""" super().__init__() self._expand_id = 'navigate' self._expand_ext = 'html' @@ -1557,45 +1572,44 @@ def can_be_converted(self, ext): return False return ext in IMAGEABLES_SVG or ext in IMAGEABLES_GS or ext in IMAGEABLES_SIMPLE - def get_image_for_cat(self, cat): - img = None - if cat in CAT_REP and self.convert_command is not None: - outs_rep = CAT_REP[cat] - rep_file = None - # Look in all outputs - for o in RegOutput.get_outputs(): - if o.type in outs_rep: - out_dir = get_output_dir(o.dir, o, dry=True) - targets = o.get_targets(out_dir) - for tg in targets: - ext = os.path.splitext(tg)[1][1:].lower() - if os.path.isfile(tg) and self.can_be_converted(ext): - rep_file = tg + def get_image_for_cat(self, cat, pname): + if self.display_category_images: + img = None + if cat in CAT_REP and self.convert_command is not None: + outs_rep = CAT_REP[cat] + rep_file = None + # Look in all outputs + for o in RegOutput.get_outputs(): + if o.type in outs_rep: + out_dir = get_output_dir(o.dir, o, dry=True) + targets = o.get_targets(out_dir) + for tg in targets: + ext = os.path.splitext(tg)[1][1:].lower() + if os.path.isfile(tg) and self.can_be_converted(ext): + rep_file = tg + break + if rep_file: break - if rep_file: - break - if rep_file: - cat_img, _ = self.get_image_for_file(rep_file, cat, no_icon=True) - cat_img = (f''' - <div class="category-title"> - <img src="{cat_img}" alt="{cat}" width="{int(BIG_ICON*0.6)}" height="{int(BIG_ICON*0.6)}"> - <br><span class="category-title">{cat}</span> - </div> - ''') + if rep_file: + cat_img, _ = self.get_image_for_file(rep_file, cat, no_icon=True, is_category=True, category_path=pname) + return cat_img + + if cat in CAT_IMAGE: + img = self.copy(CAT_IMAGE[cat], BIG_ICON) + # Include the category name with the category-title class + cat_img = ( + f'''<div class="category-box" onclick="location.href='{pname}'">''' + f' <img src="{img}" alt="{cat}" width="{BIG_ICON}" height="{BIG_ICON}">' + f' <p class="category-title">{cat}</p>' + f'</div>' + ) return cat_img - - if cat in CAT_IMAGE: - img = self.copy(CAT_IMAGE[cat], BIG_ICON) - # Include the category name with the category-title class - cat_img = ( - f'<div class="category-title">' - f' <img src="{img}" alt="{cat}" width="{int(BIG_ICON*0.6)}" height="{int(BIG_ICON*0.6)}">' - f' <br><span class="category-title">{cat}</span>' - f'</div>' - ) - return cat_img - return f'<div class="category-title">{cat}</div>' # Fallback if no image + return (f''' + <div class="category-box" onclick="location.href='{pname}'"> + <p class="category-title">{cat}</p> + </div> + ''') # Fallback if no image def compose_image(self, file, ext, img, out_name, no_icon=False): if not os.path.isfile(file): @@ -1637,7 +1651,8 @@ def compose_image(self, file, ext, img, out_name, no_icon=False): os.remove(tmp_name) return res, fname, os.path.relpath(fname, start=self.out_dir) - def get_image_for_file(self, file, out_name, no_icon=False, image=None): + def get_image_for_file(self, file, out_name, no_icon=False, is_category=False, category_path='', image=None): + tg_rel = os.path.relpath(os.path.abspath(file), start=self.out_dir) ext = os.path.splitext(file)[1][1:].lower() wide = False # Copy the icon for this file extension @@ -1665,8 +1680,29 @@ def get_image_for_file(self, file, out_name, no_icon=False, image=None): wide = True # Now add the image with its file name as caption ext_img = '<img src="{}" alt="{}" width="{}" height="{}">'.format(img, file, width, height) - file = ('<table class="out-img"><tr><td>{}</td></tr><tr><td class="{}">{}</td></tr></table>'. - format(ext_img, 'td-normal' if no_icon else 'td-small', out_name if no_icon else file)) + # file = ('<table class="out-img"><tr><td>{}</td></tr><tr><td class="{}">{}</td></tr></table>'. + # format(ext_img, 'td-normal' if no_icon else 'td-small', out_name if no_icon else file)) + + cell_class = "wide" if wide else "" + + # Make the entire output-box clickable + file_name = file + if is_category: + file = f'''\n <div class="category-box" onclick="location.href='{category_path}'">\n''' + else: + file = f'''\n <div class="output-box {cell_class}" onclick="location.href='{tg_rel}'">\n''' + + file += (f''' {ext_img}\n''') + + if is_category: + file += (f''' <p class="category-title">{out_name}</p>\n''') + else: + file += (f''' <p class="filename">{file_name}</p>\n''') + file += (f''' <p class="output-name">{out_name}</p>\n''') + + file += ''' </div>\n''' + + return file, wide def write_kibot_version(self, f): @@ -1697,7 +1733,8 @@ def generate_cat_page_for(self, name, node, prev, category): self.write_head(f, category) name, ext = os.path.splitext(name) - f.write('<div>\n') + f.write('<div class="items-container">\n') + # f.write('<div>\n') for cat, content in node.items(): if not isinstance(content, dict): @@ -1706,13 +1743,10 @@ def generate_cat_page_for(self, name, node, prev, category): pname = name + '_' + cat + ext self.generate_page_for(content, pname, name, category + '/' + cat) - # Wrap the entire box in the <a> tag to make it clickable - f.write(f''' - <a href="{pname}" class="category-box"> - <span class="category-title">{cat}</span> - </a> - ''') + # f.write('''<div onclick="location.href='{}'">{}</div>'''.format(pname, self.get_image_for_cat(cat))) + f.write('''<div>{}</div>'''.format(self.get_image_for_cat(cat, pname))) + # f.write('</div>\n') f.write('</div>\n') # Generate outputs below the categories @@ -1772,7 +1806,6 @@ def generate_outputs(self, f, node): f.write('<div class="items-container">\n') for tg, icon in zip(targets, icons if icons else [None] * len(targets)): - tg_rel = os.path.relpath(os.path.abspath(tg), start=self.out_dir) ext = os.path.splitext(tg)[1].lower() if ext == '.md' and self.render_markdown: # Render markdown only if enabled @@ -1789,17 +1822,9 @@ def generate_outputs(self, f, node): ''') else: # Handle other files (icons, images, etc.) - img, wide = self.get_image_for_file(tg, oname, image=icon) - - cell_class = "wide" if wide else "" + output_cell, wide = self.get_image_for_file(tg, oname, image=icon) - # Make the entire output-box clickable - f.write(f''' - <div class="output-box {cell_class}" onclick="location.href='{tg_rel}'"> - <a href="{tg_rel}" class="filename">{img}</a> - <p class="output-name">{oname}</p> - </div> - ''') + f.write(output_cell) # Close the items container and category box f.write('</div>\n</div>\n') From 683e96cd60210fa52ad96e29c73afdbbb7ebcb8b Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 18 Jan 2025 15:08:07 +0100 Subject: [PATCH 10/14] [BaseOutput][Added] note about root path for category --- kibot/out_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kibot/out_base.py b/kibot/out_base.py index de8057517..626bded9e 100644 --- a/kibot/out_base.py +++ b/kibot/out_base.py @@ -87,7 +87,8 @@ def __init__(self): """ [string|list(string)=''] {comma_sep} The category for this output. If not specified an internally defined category is used. Categories looks like file system paths, i.e. **PCB/fabrication/gerber**. - The categories are currently used for `navigate_results` """ + Using '.' or './' as a category puts the file at the root. + The categories are currently used for `navigate_results` and `navigate_results_rb` """ self.priority = 50 """ [0,100] Priority for this output. High priority outputs are created first. Internally we use 10 for low priority, 90 for high priority and 50 for most outputs """ From b4b5d484ff4c84fcea458671a4fe6ac4c884bd8c Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 18 Jan 2025 15:09:07 +0100 Subject: [PATCH 11/14] [Navigate Results][Changed] Refractor code for multiple styles --- kibot/out_any_navigate_results.py | 505 ++++++ kibot/out_navigate_results.py | 593 ++----- kibot/out_navigate_results_modern.py | 2176 -------------------------- 3 files changed, 661 insertions(+), 2613 deletions(-) create mode 100644 kibot/out_any_navigate_results.py delete mode 100644 kibot/out_navigate_results_modern.py diff --git a/kibot/out_any_navigate_results.py b/kibot/out_any_navigate_results.py new file mode 100644 index 000000000..9e5da1c45 --- /dev/null +++ b/kibot/out_any_navigate_results.py @@ -0,0 +1,505 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022-2024 Salvador E. Tropea +# Copyright (c) 2022-2024 Instituto Nacional de Tecnología Industrial +# License: AGPL-3.0 +# Project: KiBot (formerly KiPlot) +# The Assembly image is a composition from Pixlok and oNline Web Fonts +# The rest are KiCad icons +import base64 +import os +import subprocess +import pprint +import re +from shutil import copy2 +from math import ceil +from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H +from .error import KiPlotConfigurationError +from .gs import GS +from .optionable import Optionable +from .out_base import VariantOptions +from .kiplot import config_output, get_output_dir, run_command +from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS, read_png, force_list +from .pre_base import BasePreFlight +from .registrable import RegOutput +from .macros import macros, document # noqa: F401 +from . import log, __version__ + +logger = log.get_logger() +CAT_IMAGE = {'PCB': 'pcbnew', + 'Schematic': 'eeschema', + 'Compress': 'zip', + 'fabrication': 'fabrication', + 'export': 'export', + 'assembly': 'assembly_simple', + 'repair': 'repair', + 'docs': 'project', + 'BoM': 'bom', + '3D': '3d', + 'gerber': 'gerber', + 'drill': 'load_drill', + 'Auxiliar': 'repair'} +EXT_IMAGE = {'gbr': 'file_gbr', + 'gtl': 'file_gbr', + 'gtp': 'file_gbr', + 'gbo': 'file_gbr', + 'gto': 'file_gbr', + 'gbs': 'file_gbr', + 'gbl': 'file_gbr', + 'gts': 'file_gbr', + 'gml': 'file_gbr', + 'gm1': 'file_gbr', + 'gbrjob': 'file_gerber_job', + 'brd': 'file_brd', + 'bz2': 'file_bz2', + 'dxf': 'file_dxf', + 'cad': 'file_cad', + 'drl': 'file_drl', + 'pdf': 'file_pdf', + 'txt': 'file_txt', + 'pos': 'file_pos', + 'csv': 'file_csv', + 'svg': 'file_svg', + 'eps': 'file_eps', + 'png': 'file_png', + 'jpg': 'file_jpg', + 'plt': 'file_plt', + 'ps': 'file_ps', + 'rar': 'file_rar', + 'scad': 'file_scad', + 'stl': 'file_stl', + 'step': 'file_stp', + 'stp': 'file_stp', + 'wrl': 'file_wrl', + 'html': 'file_html', + 'css': 'file_css', + 'xml': 'file_xml', + 'tsv': 'file_tsv', + 'xlsx': 'file_xlsx', + 'xyrs': 'file_xyrs', + 'xz': 'file_xz', + 'gz': 'file_gz', + 'tar': 'file_tar', + 'zip': 'file_zip', + 'kicad_pcb': 'pcbnew', + 'sch': 'eeschema', + 'kicad_sch': 'eeschema', + 'blend': 'file_blend', + 'pcb3d': 'file_pcb3d', + 'json': 'file_json'} +for i in range(31): + n = str(i) + EXT_IMAGE['gl'+n] = 'file_gbr' + EXT_IMAGE['g'+n] = 'file_gbr' + EXT_IMAGE['gp'+n] = 'file_gbr' +CAT_REP = {'PCB': ['pdf_pcb_print', 'svg_pcb_print', 'pcb_print'], + 'Schematic': ['pdf_sch_print', 'svg_sch_print']} +IMAGEABLES_SIMPLE = {'png', 'jpg'} +IMAGEABLES_GS = {'pdf', 'eps', 'ps'} +IMAGEABLES_SVG = {'svg'} + + +def _run_command(cmd): + logger.debug('- Executing: '+GS.pasteable_cmd(cmd)) + try: + cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + if e.output: + logger.debug('Output from command: '+e.output.decode()) + logger.non_critical_error(f'Failed to run {cmd[0]}, error {e.returncode}') + return False + if cmd_output.strip(): + logger.debug('- Output from command:\n'+cmd_output.decode()) + return True + + +class Any_Navigate_ResultsOptions(VariantOptions): + def __init__(self): + with document: + self.output = GS.def_global_output + """ *Filename for the output (%i=html, %x=navigate) """ + self.link_from_root = '' + """ *The name of a file to create at the main output directory linking to the home page """ + self.skip_not_run = False + """ Skip outputs with `run_by_default: false` """ + self.logo = Optionable + """ [string|boolean=''] PNG file to use as logo, use false to remove. + The KiBot logo is used by default """ + self.logo_url = 'https://github.com/INTI-CMNB/KiBot/' + """ Target link when clicking the logo """ + self.logo_force_height = -1 + """ Force logo height in px. Useful to get consistent heights across different logos. """ + self.title = '' + """ Title for the page, when empty KiBot will try using the schematic or PCB title. + If they are empty the name of the project, schematic or PCB file is used. + You can use %X values and KiCad variables here """ + self.title_url = Optionable + """ [string|boolean=''] Target link when clicking the title, use false to remove. + KiBot will try with the origin of the current git repo when empty """ + self.nav_bar = True + """ Add a side navigation bar to quickly access to the outputs """ + self.render_markdown = True + """ If True, markdown files are rendered; otherwise, they are treated like other files """ + self.header = True + """ Add a header containing information for the project """ + self.display_kibot_version = True + """ If True, display the KiBot version at the bottom of each page """ + self.display_category_images = True + """ If True, we try to display images for categories according to the category type""" + super().__init__() + self._variant_name = self._find_variant_name() + self._expand_id = 'navigate' + self._expand_ext = 'html' + self._default_logo_scale = 0.5 + self._style = '' + self._title_height = 30 + self._big_icon = 256 + self._mid_icon = 64 + + def config(self, parent): + super().config(parent) + # Logo + if isinstance(self.logo, bool): + self.logo = '' if self.logo else None + elif self.logo: + self.logo = os.path.abspath(self.logo) + if not os.path.isfile(self.logo): + raise KiPlotConfigurationError('Missing logo file `{}`'.format(self.logo)) + try: + self._logo_data, self._logo_w, self._logo_h, _ = read_png(self.logo, logger) + except TypeError as e: + raise KiPlotConfigurationError(f'Only PNG images are supported for the logo ({e})') + if self.logo == '': + # Internal logo + self._logo_w = int(KIBOT_LOGO_W*self._default_logo_scale) + self._logo_h = int(KIBOT_LOGO_H*self._default_logo_scale) + self._logo_data = base64.b64decode(KIBOT_LOGO) + elif self.logo is None: + self._logo_w = self._logo_h = 0 + self._logo_data = '' + if self.logo_force_height > 0: + self._logo_w = self.logo_force_height/self._logo_h*self._logo_w + self._logo_h = self.logo_force_height + # Title URL + if isinstance(self.title_url, bool): + self.title_url = '' if self.title_url else None + + def add_to_tree(self, cat, out, o_tree): + if cat == '.' or cat == './': + # Place output directly at the root level + o_tree[out.name] = out + else: + cat = cat.split('/') + node = o_tree + for c in cat: + if c not in node: + # New category + node[c] = {} + node = node[c] + node[out.name] = out + + def svg_to_png(self, svg_file, png_file, width): + cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', png_file, svg_file] + return _run_command(cmd) + + def copy(self, img, width): + """ Copy an SVG icon to the images/ dir. + Tries to convert it to PNG. """ + img_w = "{}_{}".format(os.path.basename(img), width) + if img_w in self.copied_images: + # Already copied, just return its name + return self.copied_images[img_w] + src = os.path.join(self.img_src_dir, img+'.svg') if not img.endswith('.svg') else img + dst = os.path.join(self.out_dir, 'images', img_w) + id = img_w + if self.rsvg_command is not None and self.svg_to_png(src, dst+'.png', width): + img_w += '.png' + else: + copy2(src, dst+'.svg') + img_w += '.svg' + name = os.path.join('images', img_w) + self.copied_images[id] = name + return name + + def can_be_converted(self, ext): + if ext in IMAGEABLES_SVG and self.rsvg_command is None: + logger.warning(W_MISSTOOL+"Missing SVG to PNG converter") + return False + if ext in IMAGEABLES_GS and not self.ps2img_avail: + logger.warning(W_MISSTOOL+"Missing PS/PDF to PNG converter") + return False + if ext in IMAGEABLES_SIMPLE and self.convert_command is None: + logger.warning(W_MISSTOOL+"Missing ImageMagick converter") + return False + return ext in IMAGEABLES_SVG or ext in IMAGEABLES_GS or ext in IMAGEABLES_SIMPLE + + def compose_image(self, file, ext, img, out_name, no_icon=False): + if not os.path.isfile(file): + logger.warning(W_NOTYET+"{} not yet generated, using an icon".format(os.path.relpath(file))) + return False, None, None + if self.convert_command is None: + return False, None, None + # Create a unique name using the output name and the generated file name + bfname = os.path.splitext(os.path.basename(file))[0] + fname = os.path.join(self.out_dir, 'images', out_name+'_'+bfname+'.png') + # Full path for the icon image + icon = os.path.join(self.out_dir, img) + if ext == 'pdf': + # Only page 1 + file += '[0]' + if ext == 'svg': + tmp_name = GS.tmp_file(suffix='.png') + logger.debug('Temporal convert: {} -> {}'.format(file, tmp_name)) + if not self.svg_to_png(file, tmp_name, self._big_icon): + return False, None, None + file = tmp_name + cmd = [self.convert_command, file, + # Size for the big icons (width) + '-resize', str(self._big_icon)+'x'] + if ext == 'ps': + # ImageMagick 6.9.11 (and also the one in Debian 11) rotates the PS + cmd.extend(['-rotate', '90']) + if not no_icon: + cmd.extend([ # Add the file type icon + icon, + # At the bottom right + '-gravity', 'south-east', + # This is a composition, not 2 images + '-composite']) + cmd.append(fname) + res = _run_command(cmd) + if ext == 'svg': + logger.debug('Removing temporal {}'.format(tmp_name)) + os.remove(tmp_name) + return res, fname, os.path.relpath(fname, start=self.out_dir) + + def write_kibot_version(self, f): + if self.display_kibot_version: + f.write('<p class="generator">Generated by <a href="https://github.com/INTI-CMNB/KiBot/">KiBot</a> v{}</p>\n'. + format(__version__)) + + def write_head(self, f, title): + f.write('<!DOCTYPE html>\n') + f.write('<html lang="en">\n') + f.write('<head>\n') + f.write(' <title>{}\n'.format(title if title else 'Main page')) + f.write(' \n') # UTF-8 encoding for unicode support + f.write(' \n') + f.write(' \n') + # Include Markdown-it + if self.render_markdown: + f.write(' \n') + f.write('\n') + f.write('\n') + f.write(self.navbar) + f.write(self.top_menu) + f.write('
    \n') + + def generate_cat_page_for(self, name, node, prev, category): + raise NotImplementedError + + def adjust_image_paths(self, md_content, current_dir, html_output_dir): + + image_pattern = r'!\[.*?\]\((.*?)\)' # Markdown image paths: ![Alt text](path/to/image.png) + html_img_pattern = r']*src="([^"]+)"' # HTML img src="path/to/image.png" + + def replace_path(match): + original_path = match.group(1) + + # Skip absolute URLs or paths + if original_path.startswith(('http://', 'https://', '/')): + return match.group(0) + + # Convert relative path to absolute and back to relative for the HTML output directory + abs_path = os.path.abspath(os.path.join(current_dir, original_path)) + rel_path = os.path.relpath(abs_path, html_output_dir) + + # Replace the path with the new relative path + return match.group(0).replace(original_path, rel_path) + + # Replace Markdown image paths + md_content = re.sub(image_pattern, replace_path, md_content) + + # Replace HTML tag paths + md_content = re.sub(html_img_pattern, replace_path, md_content) + + return md_content + + def generate_outputs(self, f, node): + raise NotImplementedError + + def generate_end_page_for(self, name, node, prev, category): + raise NotImplementedError + + def generate_page_for(self, node, name, prev=None, category=''): + raise NotImplementedError + + def get_targets(self, out_dir): + # Listing all targets is too complex, we list the most relevant + # This is good enough to compress the result + name = self._parent.expand_filename(out_dir, self.output) + files = [os.path.join(out_dir, 'images'), + os.path.join(out_dir, 'styles.css'), + os.path.join(out_dir, 'favicon.ico')] + if self.render_markdown: + files.append(os.path.join(out_dir, 'markdown-it.min.js')) + if self.link_from_root: + files.append(os.path.join(GS.out_dir, self.link_from_root)) + self.out_dir = out_dir + self.get_html_names(self.create_tree(), name, files) + return files + + def get_html_names_cat(self, name, node, prev, category, files): + files.append(os.path.join(self.out_dir, name)) + name, ext = os.path.splitext(name) + for cat, content in node.items(): + if not isinstance(content, dict): + continue + pname = name+'_'+cat+ext + self.get_html_names(content, pname, files, name, category+'/'+cat) + + def get_html_names(self, node, name, files, prev=None, category=''): + if isinstance(list(node.values())[0], dict): + self.get_html_names_cat(name, node, prev, category, files) + else: + files.append(os.path.join(self.out_dir, name)) + + def get_html_names_for_path(self, category_path, name, ext): + files = [] + node = self.create_tree() + self.get_html_names_cat(name, node, None, category_path, files) + for file_path in files: + if category_path.replace('/', '_') in file_path: + return os.path.basename(file_path) + ext + return None + + def create_tree(self): + o_tree = {} + BasePreFlight.configure_all() + for n in BasePreFlight.get_in_use_names(): + pre = BasePreFlight.get_preflight(n) + cat = force_list(pre.get_category()) + if not cat: + continue + for c in cat: + self.add_to_tree(c, pre, o_tree) + for o in RegOutput.get_outputs(): + if not o.run_by_default and self.skip_not_run: + # Skip outputs that aren't generated in a regular run + continue + config_output(o) + cat = o.category + if cat is None: + continue + for c in cat: + self.add_to_tree(c, o, o_tree) + return o_tree + + def generate_navbar_one(self, node, lvl, name, ext): + raise NotImplementedError + + def generate_navbar(self, node, name): + raise NotImplementedError + + def generate_top_menu(self, category=''): + raise NotImplementedError + + def solve_title(self): + base_title = None + if GS.sch: + base_title = GS.sch.get_title() + if GS.board and not base_title: + tb = GS.board.GetTitleBlock() + base_title = tb.GetTitle() + if not base_title: + base_title = GS.pro_basename or GS.sch_basename or GS.pcb_basename or 'Unknown' + text = self.expand_filename_sch(self.title if self.title else '+') + if text[0] == '+': + text = base_title+text[1:] + self._solved_title = text + # Now the URL + if self.title_url is not None and not self.title_url: + # Empty but not None + self._git_command = self.check_tool('Git') + if self._git_command: + res = '' + try: + res = run_command([self._git_command, 'remote', 'get-url', 'origin'], just_raise=True) + except subprocess.CalledProcessError: + pass + if res: + self.title_url = res + + def solve_revision(self): + base_rev = None + if GS.sch: + GS.load_sch() + GS.load_sch_title_block() + base_rev = GS.sch_rev + if GS.board and not base_rev: + tb = GS.board.GetTitleBlock() + base_rev = tb.GetRevision() + if not base_rev: + base_rev = 'Unknown' + self._solved_revision = base_rev + + def solve_company(self): + base_comp = None + if GS.sch: + GS.load_sch() + GS.load_sch_title_block() + base_comp = GS.sch_comp + if GS.board and not base_comp: + tb = GS.board.GetTitleBlock() + base_comp = tb.GetCompany() + if not base_comp: + base_comp = 'Unknown' + self._solved_company = base_comp + + def run(self, name): + self.out_dir = os.path.dirname(name) + self.img_src_dir = GS.get_resource_path('images') + self.js_src_dir = GS.get_resource_path('navigate_results') + self.img_dst_dir = os.path.join(self.out_dir, 'images') + os.makedirs(self.img_dst_dir, exist_ok=True) + self.copied_images = {} + name = os.path.basename(name) + # Create a tree with all the outputs + o_tree = self.create_tree() + logger.debug('Collected outputs:\n'+pprint.pformat(o_tree)) + if not o_tree: + logger.warning(W_NOOUTPUTS+'No outputs for navigate results') + return + with open(os.path.join(self.out_dir, 'styles.css'), 'wt') as f: + if not self.header: + top_margin = 0 if not self.nav_bar else self._title_height + else: + top_margin = str(max(self._logo_h, self._title_height)) + f.write(self._style.replace('@TOP_MAR@', str(top_margin))) + self.rsvg_command = self.check_tool('rsvg1') + self.convert_command = self.check_tool('ImageMagick') + self.ps2img_avail = self.check_tool('Ghostscript') + # Create the pages + self.home = name + self.back_img = self.copy('back', self._mid_icon) + self.home_img = self.copy('home', self._mid_icon) + copy2(os.path.join(self.img_src_dir, 'favicon.ico'), os.path.join(self.out_dir, 'favicon.ico')) + if self.render_markdown: + copy2(os.path.join(self.js_src_dir, 'markdown-it.min.js'), os.path.join(self.out_dir, 'markdown-it.min.js')) + # Copy the logo image + if self.logo is not None and self.header: + with open(os.path.join(self.out_dir, 'images', 'logo.png'), 'wb') as f: + f.write(self._logo_data) + self.solve_title() + self.solve_company() + self.solve_revision() + self.navbar = self.generate_navbar(o_tree, name) if self.nav_bar else '' + self.top_menu = self.generate_top_menu() if self.nav_bar or self.header else '' + self.generate_page_for(o_tree, name) + # Link it? + if self.link_from_root: + redir_file = os.path.join(GS.out_dir, self.link_from_root) + rel_start = os.path.relpath(os.path.join(self.out_dir, name), start=GS.out_dir) + logger.debug('Creating redirector: {} -> {}'.format(redir_file, rel_start)) + with open(redir_file, 'wt') as f: + f.write('\n\n'.format(rel_start)) + f.write('\n') diff --git a/kibot/out_navigate_results.py b/kibot/out_navigate_results.py index 805433683..91cf47a55 100644 --- a/kibot/out_navigate_results.py +++ b/kibot/out_navigate_results.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022-2024 Salvador E. Tropea -# Copyright (c) 2022-2024 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2022-2025 Salvador E. Tropea +# Copyright (c) 2022-2025 Nguyen Vincent +# Copyright (c) 2022-2025 Instituto Nacional de Tecnología Industrial +# Contributed by Nguyen Vincent (@nguyen-v) # License: AGPL-3.0 # Project: KiBot (formerly KiPlot) # The Assembly image is a composition from Pixlok and oNline Web Fonts @@ -20,100 +22,19 @@ - from: Git role: Find origin url """ -import base64 import os -import subprocess -import pprint -from shutil import copy2 from math import ceil -from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H -from .error import KiPlotConfigurationError -from .gs import GS -from .optionable import Optionable, BaseOptions -from .kiplot import config_output, get_output_dir, run_command -from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS, read_png, force_list -from .pre_base import BasePreFlight from .registrable import RegOutput +from .out_any_navigate_results import Any_Navigate_ResultsOptions, CAT_IMAGE, EXT_IMAGE, CAT_REP from .macros import macros, document, output_class # noqa: F401 -from . import log, __version__ +from .kiplot import get_output_dir +from .misc import read_png +from . import log logger = log.get_logger() -CAT_IMAGE = {'PCB': 'pcbnew', - 'Schematic': 'eeschema', - 'Compress': 'zip', - 'fabrication': 'fabrication', - 'export': 'export', - 'assembly': 'assembly_simple', - 'repair': 'repair', - 'docs': 'project', - 'BoM': 'bom', - '3D': '3d', - 'gerber': 'gerber', - 'drill': 'load_drill', - 'Auxiliar': 'repair'} -EXT_IMAGE = {'gbr': 'file_gbr', - 'gtl': 'file_gbr', - 'gtp': 'file_gbr', - 'gbo': 'file_gbr', - 'gto': 'file_gbr', - 'gbs': 'file_gbr', - 'gbl': 'file_gbr', - 'gts': 'file_gbr', - 'gml': 'file_gbr', - 'gm1': 'file_gbr', - 'gbrjob': 'file_gerber_job', - 'brd': 'file_brd', - 'bz2': 'file_bz2', - 'dxf': 'file_dxf', - 'cad': 'file_cad', - 'drl': 'file_drl', - 'pdf': 'file_pdf', - 'txt': 'file_txt', - 'pos': 'file_pos', - 'csv': 'file_csv', - 'svg': 'file_svg', - 'eps': 'file_eps', - 'png': 'file_png', - 'jpg': 'file_jpg', - 'plt': 'file_plt', - 'ps': 'file_ps', - 'rar': 'file_rar', - 'scad': 'file_scad', - 'stl': 'file_stl', - 'step': 'file_stp', - 'stp': 'file_stp', - 'wrl': 'file_wrl', - 'html': 'file_html', - 'css': 'file_css', - 'xml': 'file_xml', - 'tsv': 'file_tsv', - 'xlsx': 'file_xlsx', - 'xyrs': 'file_xyrs', - 'xz': 'file_xz', - 'gz': 'file_gz', - 'tar': 'file_tar', - 'zip': 'file_zip', - 'kicad_pcb': 'pcbnew', - 'sch': 'eeschema', - 'kicad_sch': 'eeschema', - 'blend': 'file_blend', - 'pcb3d': 'file_pcb3d', - 'json': 'file_json'} -for i in range(31): - n = str(i) - EXT_IMAGE['gl'+n] = 'file_gbr' - EXT_IMAGE['g'+n] = 'file_gbr' - EXT_IMAGE['gp'+n] = 'file_gbr' -CAT_REP = {'PCB': ['pdf_pcb_print', 'svg_pcb_print', 'pcb_print'], - 'Schematic': ['pdf_sch_print', 'svg_sch_print']} -BIG_ICON = 256 -MID_ICON = 64 + OUT_COLS = 12 -BIG_2_MID_REL = int(ceil(BIG_ICON/MID_ICON)) -IMAGEABLES_SIMPLE = {'png', 'jpg'} -IMAGEABLES_GS = {'pdf', 'eps', 'ps'} -IMAGEABLES_SVG = {'svg'} -TITLE_HEIGHT = 30 + STYLE = """ .cat-table { margin-left: auto; margin-right: auto; } .cat-table td { padding: 20px 24px; } @@ -138,6 +59,7 @@ .output-table th { padding: 10px 12px; } .output-table td { padding: 5px 7px; } .out-cell { width: 128px; text-align: center } +.out-cell-md { width: 128px;} .out-img { text-align: center; margin-left: auto; margin-right: auto; } .cat-img { text-align: center; margin-left: auto; margin-right: auto; } .td-small { text-align: center; font-size: 0.6em; } @@ -208,6 +130,37 @@ width: 100%; /* Full width */ background-color: white; /* Otherwise is transparent and overlaps */ } +.markdown-content { + font-family: Roboto, sans-serif; + line-height: 1.6; + padding: 15px; + border-radius: 5px; + max-width: 100%; + white-space: pre-wrap; /* Handle preformatted text */ + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} +.markdown-content table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} +.markdown-content pre { + padding: 10px; + border-radius: 5px; + overflow-x: auto; +} +.markdown-content code { + padding: 2px 5px; + border-radius: 3px; + font-family: 'Courier New', Courier, monospace; +} +.markdown-content img { + max-width: 100%; + height: auto; + display: block; + margin: 10px auto; +} """ SCRIPT = """ """ - -def _run_command(cmd): - logger.debug('- Executing: '+GS.pasteable_cmd(cmd)) - try: - cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - if e.output: - logger.debug('Output from command: '+e.output.decode()) - logger.non_critical_error(f'Failed to run {cmd[0]}, error {e.returncode}') - return False - if cmd_output.strip(): - logger.debug('- Output from command:\n'+cmd_output.decode()) - return True +SCRIPT_MARKDOWN = """ + +""" -class Navigate_ResultsOptions(BaseOptions): +class Navigate_ResultsOptions(Any_Navigate_ResultsOptions): def __init__(self): - with document: - self.output = GS.def_global_output - """ *Filename for the output (%i=html, %x=navigate) """ - self.link_from_root = '' - """ *The name of a file to create at the main output directory linking to the home page """ - self.skip_not_run = False - """ Skip outputs with `run_by_default: false` """ - self.logo = Optionable - """ [string|boolean=''] PNG file to use as logo, use false to remove. - The KiBot logo is used by default """ - self.logo_url = 'https://github.com/INTI-CMNB/KiBot/' - """ Target link when clicking the logo """ - self.title = '' - """ Title for the page, when empty KiBot will try using the schematic or PCB title. - If they are empty the name of the project, schematic or PCB file is used. - You can use %X values and KiCad variables here """ - self.title_url = Optionable - """ [string|boolean=''] Target link when clicking the title, use false to remove. - KiBot will try with the origin of the current git repo when empty """ - self.nav_bar = True - """ Add a side navigation bar to quickly access to the outputs """ - self.header = True - """ Add a header containing information for the project """ super().__init__() - self._expand_id = 'navigate' - self._expand_ext = 'html' - - def config(self, parent): - super().config(parent) - # Logo - if isinstance(self.logo, bool): - self.logo = '' if self.logo else None - elif self.logo: - self.logo = os.path.abspath(self.logo) - if not os.path.isfile(self.logo): - raise KiPlotConfigurationError('Missing logo file `{}`'.format(self.logo)) - try: - self._logo_data, self._logo_w, self._logo_h, _ = read_png(self.logo, logger) - except TypeError as e: - raise KiPlotConfigurationError(f'Only PNG images are supported for the logo ({e})') - if self.logo == '': - # Internal logo - self._logo_w = int(KIBOT_LOGO_W/2) - self._logo_h = int(KIBOT_LOGO_H/2) - self._logo_data = base64.b64decode(KIBOT_LOGO) - elif self.logo is None: - self._logo_w = self._logo_h = 0 - self._logo_data = '' - # Title URL - if isinstance(self.title_url, bool): - self.title_url = '' if self.title_url else None - - def add_to_tree(self, cat, out, o_tree): - # Add `out` to `o_tree` in the `cat` category - cat = cat.split('/') - node = o_tree - for c in cat: - if c not in node: - # New one - node[c] = {} - node = node[c] - node[out.name] = out - - def svg_to_png(self, svg_file, png_file, width): - cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', png_file, svg_file] - return _run_command(cmd) - - def copy(self, img, width): - """ Copy an SVG icon to the images/ dir. - Tries to convert it to PNG. """ - img_w = "{}_{}".format(os.path.basename(img), width) - if img_w in self.copied_images: - # Already copied, just return its name - return self.copied_images[img_w] - src = os.path.join(self.img_src_dir, img+'.svg') if not img.endswith('.svg') else img - dst = os.path.join(self.out_dir, 'images', img_w) - id = img_w - if self.rsvg_command is not None and self.svg_to_png(src, dst+'.png', width): - img_w += '.png' - else: - copy2(src, dst+'.svg') - img_w += '.svg' - name = os.path.join('images', img_w) - self.copied_images[id] = name - return name - - def can_be_converted(self, ext): - if ext in IMAGEABLES_SVG and self.rsvg_command is None: - logger.warning(W_MISSTOOL+"Missing SVG to PNG converter") - return False - if ext in IMAGEABLES_GS and not self.ps2img_avail: - logger.warning(W_MISSTOOL+"Missing PS/PDF to PNG converter") - return False - if ext in IMAGEABLES_SIMPLE and self.convert_command is None: - logger.warning(W_MISSTOOL+"Missing ImageMagick converter") - return False - return ext in IMAGEABLES_SVG or ext in IMAGEABLES_GS or ext in IMAGEABLES_SIMPLE + self._default_logo_scale = 0.5 + self._style = STYLE + self._big_2_mid_rel = int(ceil(self._big_icon/self._mid_icon)) def get_image_for_cat(self, cat): - img = None - # Check if we have an output that can represent this category - if cat in CAT_REP and self.convert_command is not None: - outs_rep = CAT_REP[cat] - rep_file = None - # Look in all outputs - for o in RegOutput.get_outputs(): - # Is this one that can be used to represent it? - if o.type in outs_rep: - out_dir = get_output_dir(o.dir, o, dry=True) - targets = o.get_targets(out_dir) - # Look the output targets - for tg in targets: - ext = os.path.splitext(tg)[1][1:].lower() - # Can be converted to an image? - if os.path.isfile(tg) and self.can_be_converted(ext): - rep_file = tg + if self.display_category_images: + img = None + # Check if we have an output that can represent this category + if cat in CAT_REP and self.convert_command is not None: + outs_rep = CAT_REP[cat] + rep_file = None + # Look in all outputs + for o in RegOutput.get_outputs(): + # Is this one that can be used to represent it? + if o.type in outs_rep: + out_dir = get_output_dir(o.dir, o, dry=True) + targets = o.get_targets(out_dir) + # Look the output targets + for tg in targets: + ext = os.path.splitext(tg)[1][1:].lower() + # Can be converted to an image? + if os.path.isfile(tg) and self.can_be_converted(ext): + rep_file = tg + break + if rep_file: break - if rep_file: - break - if rep_file: - cat, _ = self.get_image_for_file(rep_file, cat, no_icon=True) - return cat - if cat in CAT_IMAGE: - img = self.copy(CAT_IMAGE[cat], BIG_ICON) - cat_img = '{}'.format(img, cat, BIG_ICON, BIG_ICON) - cat = ('
    {}
    {}
    '. - format(cat_img, cat)) + if rep_file: + cat, _ = self.get_image_for_file(rep_file, cat, no_icon=True) + return cat + if cat in CAT_IMAGE: + img = self.copy(CAT_IMAGE[cat], self._big_icon) + cat_img = '{}'.format(img, cat, self._big_icon, self._big_icon) + cat = ('
    {}
    {}
    '.format(cat_img, cat)) return cat - def compose_image(self, file, ext, img, out_name, no_icon=False): - if not os.path.isfile(file): - logger.warning(W_NOTYET+"{} not yet generated, using an icon".format(os.path.relpath(file))) - return False, None, None - if self.convert_command is None: - return False, None, None - # Create a unique name using the output name and the generated file name - bfname = os.path.splitext(os.path.basename(file))[0] - fname = os.path.join(self.out_dir, 'images', out_name+'_'+bfname+'.png') - # Full path for the icon image - icon = os.path.join(self.out_dir, img) - if ext == 'pdf': - # Only page 1 - file += '[0]' - if ext == 'svg': - tmp_name = GS.tmp_file(suffix='.png') - logger.debug('Temporal convert: {} -> {}'.format(file, tmp_name)) - if not self.svg_to_png(file, tmp_name, BIG_ICON): - return False, None, None - file = tmp_name - cmd = [self.convert_command, file, - # Size for the big icons (width) - '-resize', str(BIG_ICON)+'x'] - if ext == 'ps': - # ImageMagick 6.9.11 (and also the one in Debian 11) rotates the PS - cmd.extend(['-rotate', '90']) - if not no_icon: - cmd.extend([ # Add the file type icon - icon, - # At the bottom right - '-gravity', 'south-east', - # This is a composition, not 2 images - '-composite']) - cmd.append(fname) - res = _run_command(cmd) - if ext == 'svg': - logger.debug('Removing temporal {}'.format(tmp_name)) - os.remove(tmp_name) - return res, fname, os.path.relpath(fname, start=self.out_dir) - def get_image_for_file(self, file, out_name, no_icon=False, image=None): ext = os.path.splitext(file)[1][1:].lower() wide = False # Copy the icon for this file extension icon_name = 'folder' if os.path.isdir(file) else EXT_IMAGE.get(ext, 'unknown') - img = self.copy(image or icon_name, MID_ICON) + img = self.copy(image or icon_name, self._mid_icon) # Full name for the file file_full = file # Just the file, to display it file = os.path.basename(file) # The icon size - height = width = MID_ICON + height = width = self._mid_icon # Check if this file can be represented by an image if self.can_be_converted(ext): # Try to compose the image of the file with the icon @@ -462,31 +287,16 @@ def add_back_home(self, f, prev): f.write('') f.write(' ') f.write(' '. - format(prev, self.back_img, MID_ICON, MID_ICON)) + format(prev, self.back_img, self._mid_icon, self._mid_icon)) f.write(' '. - format(self.home, self.home_img, MID_ICON, MID_ICON)) + format(self.home, self.home_img, self._mid_icon, self._mid_icon)) f.write(' ') f.write('') - f.write('

    Generated by KiBot v{}

    \n'. - format(__version__)) + self.write_kibot_version(f) f.write('
    \n') if self.nav_bar: f.write(SCRIPT) - def write_head(self, f, title): - f.write('\n') - f.write('\n') - f.write('\n') - f.write(' {}\n'.format(title if title else 'Main page')) - f.write(' \n') # UTF-8 encoding for unicode support - f.write(' \n') - f.write(' \n') - f.write('\n') - f.write('\n') - f.write(self.navbar) - f.write(self.top_menu) - f.write('
    \n') - def generate_cat_page_for(self, name, node, prev, category): logger.debug('- Categories: '+str(node.keys())) with open(os.path.join(self.out_dir, name), 'wt') as f: @@ -511,6 +321,8 @@ def generate_cat_page_for(self, name, node, prev, category): acc += 1 f.write('\n\n') self.generate_outputs(f, node) + if self.render_markdown: + f.write(SCRIPT_MARKDOWN) self.add_back_home(f, prev) f.write('\n\n') @@ -521,54 +333,79 @@ def generate_outputs(self, f, node): f.write(f'\n') out_name = oname.replace(' ', '_') oname = oname.replace('_', ' ') - oname = oname[0].upper()+oname[1:] + oname = oname[0].upper() + oname[1:] if out.comment: - oname += ': '+out.comment + oname += ': ' + out.comment f.write('\n'.format(OUT_COLS, oname)) out_dir = get_output_dir(out.dir, out, dry=True) f.write('\n') targets, icons = out.get_navigate_targets(out_dir) c_targets = len(targets) - # Make the icons a list with same len as targets if icons is None: - icons = [None]*c_targets + icons = [None] * c_targets else: c_icons = len(icons) if c_icons < c_targets: - icons.extend([None]*(c_targets-c_icons)) + icons.extend([None] * (c_targets - c_icons)) + if len(targets) == 1: tg_rel = os.path.relpath(os.path.abspath(targets[0]), start=self.out_dir) - img, _ = self.get_image_for_file(targets[0], out_name, image=icons[0] if icons else None) - f.write('\n'. - format(OUT_COLS, tg_rel, img)) + ext = os.path.splitext(targets[0])[1].lower() + + # Handle markdown rendering + if ext == '.md' and self.render_markdown: + with open(targets[0], 'r', encoding='utf-8') as md_file: + md_content = md_file.read() + md_content = self.adjust_image_paths(md_content, os.path.dirname(targets[0]), self.out_dir) + f.write(f''' + + ''') + else: + img, _ = self.get_image_for_file(targets[0], out_name, image=icons[0] if icons else None) + f.write('\n'. + format(OUT_COLS, tg_rel, img)) else: c = 0 for tg, icon in zip(targets, icons): - if c == OUT_COLS: - f.write('\n\n') - c = 0 - tg_rel = os.path.relpath(os.path.abspath(tg), start=self.out_dir) - img, wide = self.get_image_for_file(tg, out_name, image=icon) - # Check if we need to break this row - span = 1 - if wide: - span = BIG_2_MID_REL - remain = OUT_COLS-c - if span > remain: - f.write('\n\n'.format(remain)) - # Add a new cell - f.write('\n'.format(span, tg_rel, img)) - c = c+span + ext = os.path.splitext(tg)[1].lower() + + if ext == '.md' and self.render_markdown: + # Render Markdown + with open(tg, 'r', encoding='utf-8') as md_file: + md_content = md_file.read() + md_content = self.adjust_image_paths(md_content, os.path.dirname(tg), self.out_dir) + if c == OUT_COLS: + f.write('\n\n') + c = 0 + f.write(f''' + + ''') + c += OUT_COLS # Markdown uses full row width + else: + if c == OUT_COLS: + f.write('\n\n') + c = 0 + tg_rel = os.path.relpath(os.path.abspath(tg), start=self.out_dir) + img, wide = self.get_image_for_file(tg, out_name, image=icon) + span = 1 + if wide: + span = self._big_2_mid_rel + remain = OUT_COLS - c + if span > remain: + f.write('\n\n'.format(remain)) + f.write('\n'.format(span, tg_rel, img)) + c += span + if c < OUT_COLS: - f.write('\n'.format(OUT_COLS-c)) - f.write('\n') - # This row is just to ensure we have at least 1 cell in each column - f.write('\n') + f.write('\n'.format(OUT_COLS - c)) + f.write('\n\n') for _ in range(OUT_COLS): f.write('\n') - f.write('\n') - f.write('\n') - f.write('
    {}
    {} +
    {md_content}
    +
    {}
    {}
    +
    {md_content}
    +
    {}
    \n') + f.write('\n\n\n') def generate_end_page_for(self, name, node, prev, category): logger.debug('- Outputs: '+str(node.keys())) @@ -577,6 +414,8 @@ def generate_end_page_for(self, name, node, prev, category): name, ext = os.path.splitext(name) self.generate_outputs(f, node) self.add_back_home(f, prev) + if self.render_markdown: + f.write(SCRIPT_MARKDOWN) f.write('\n\n') def generate_page_for(self, node, name, prev=None, category=''): @@ -586,56 +425,6 @@ def generate_page_for(self, node, name, prev=None, category=''): else: self.generate_end_page_for(name, node, prev, category) - def get_targets(self, out_dir): - # Listing all targets is too complex, we list the most relevant - # This is good enough to compress the result - name = self._parent.expand_filename(out_dir, self.output) - files = [os.path.join(out_dir, 'images'), - os.path.join(out_dir, 'styles.css'), - os.path.join(out_dir, 'favicon.ico')] - if self.link_from_root: - files.append(os.path.join(GS.out_dir, self.link_from_root)) - self.out_dir = out_dir - self.get_html_names(self.create_tree(), name, files) - return files - - def get_html_names_cat(self, name, node, prev, category, files): - files.append(os.path.join(self.out_dir, name)) - name, ext = os.path.splitext(name) - for cat, content in node.items(): - if not isinstance(content, dict): - continue - pname = name+'_'+cat+ext - self.get_html_names(content, pname, files, name, category+'/'+cat) - - def get_html_names(self, node, name, files, prev=None, category=''): - if isinstance(list(node.values())[0], dict): - self.get_html_names_cat(name, node, prev, category, files) - else: - files.append(os.path.join(self.out_dir, name)) - - def create_tree(self): - o_tree = {} - BasePreFlight.configure_all() - for n in BasePreFlight.get_in_use_names(): - pre = BasePreFlight.get_preflight(n) - cat = force_list(pre.get_category()) - if not cat: - continue - for c in cat: - self.add_to_tree(c, pre, o_tree) - for o in RegOutput.get_outputs(): - if not o.run_by_default and self.skip_not_run: - # Skip outputs that aren't generated in a regular run - continue - config_output(o) - cat = o.category - if cat is None: - continue - for c in cat: - self.add_to_tree(c, o, o_tree) - return o_tree - def generate_navbar_one(self, node, lvl, name, ext): """ Recursively create a menu containing all outputs. Using ul and li items """ @@ -662,7 +451,7 @@ def generate_navbar(self, node, name): def generate_top_menu(self): # Div for the top info - fsize = f'{TITLE_HEIGHT}px' + fsize = f'{self._title_height}px' code = '
    \n' code += ' \n' code += ' \n' @@ -692,76 +481,6 @@ def generate_top_menu(self): code += '\n' return code - def solve_title(self): - base_title = None - if GS.sch: - base_title = GS.sch.get_title() - if GS.board and not base_title: - tb = GS.board.GetTitleBlock() - base_title = tb.GetTitle() - if not base_title: - base_title = GS.pro_basename or GS.sch_basename or GS.pcb_basename or 'Unknown' - text = self.expand_filename_sch(self.title if self.title else '+') - if text[0] == '+': - text = base_title+text[1:] - self._solved_title = text - # Now the URL - if self.title_url is not None and not self.title_url: - # Empty but not None - self._git_command = self.check_tool('Git') - if self._git_command: - res = '' - try: - res = run_command([self._git_command, 'remote', 'get-url', 'origin'], just_raise=True) - except subprocess.CalledProcessError: - pass - if res: - self.title_url = res - - def run(self, name): - self.out_dir = os.path.dirname(name) - self.img_src_dir = GS.get_resource_path('images') - self.img_dst_dir = os.path.join(self.out_dir, 'images') - os.makedirs(self.img_dst_dir, exist_ok=True) - self.copied_images = {} - name = os.path.basename(name) - # Create a tree with all the outputs - o_tree = self.create_tree() - logger.debug('Collected outputs:\n'+pprint.pformat(o_tree)) - if not o_tree: - logger.warning(W_NOOUTPUTS+'No outputs for navigate results') - return - with open(os.path.join(self.out_dir, 'styles.css'), 'wt') as f: - if not self.header: - top_margin = 0 if not self.nav_bar else TITLE_HEIGHT - else: - top_margin = str(max(self._logo_h, TITLE_HEIGHT)) - f.write(STYLE.replace('@TOP_MAR@', str(top_margin))) - self.rsvg_command = self.check_tool('rsvg1') - self.convert_command = self.check_tool('ImageMagick') - self.ps2img_avail = self.check_tool('Ghostscript') - # Create the pages - self.home = name - self.back_img = self.copy('back', MID_ICON) - self.home_img = self.copy('home', MID_ICON) - copy2(os.path.join(self.img_src_dir, 'favicon.ico'), os.path.join(self.out_dir, 'favicon.ico')) - # Copy the logo image - if self.logo is not None and self.header: - with open(os.path.join(self.out_dir, 'images', 'logo.png'), 'wb') as f: - f.write(self._logo_data) - self.solve_title() - self.navbar = self.generate_navbar(o_tree, name) if self.nav_bar else '' - self.top_menu = self.generate_top_menu() if self.nav_bar or self.header else '' - self.generate_page_for(o_tree, name) - # Link it? - if self.link_from_root: - redir_file = os.path.join(GS.out_dir, self.link_from_root) - rel_start = os.path.relpath(os.path.join(self.out_dir, name), start=GS.out_dir) - logger.debug('Creating redirector: {} -> {}'.format(redir_file, rel_start)) - with open(redir_file, 'wt') as f: - f.write('\n\n'.format(rel_start)) - f.write('\n') - @output_class class Navigate_Results(BaseOutput): # noqa: F821 diff --git a/kibot/out_navigate_results_modern.py b/kibot/out_navigate_results_modern.py deleted file mode 100644 index acc3223df..000000000 --- a/kibot/out_navigate_results_modern.py +++ /dev/null @@ -1,2176 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2022-2025 Salvador E. Tropea -# Copyright (c) 2022-2025 Nguyen Vincent -# Copyright (c) 2022-2025 Instituto Nacional de Tecnología Industrial -# Contributed by Nguyen Vincent (@nguyen-v) -# License: AGPL-3.0 -# Project: KiBot (formerly KiPlot) -# The Assembly image is a composition from Pixlok and oNline Web Fonts -# The rest are KiCad icons -""" -Dependencies: - - from: RSVG - role: Create outputs preview - id: rsvg1 - - from: RSVG - role: Create PNG icons - id: rsvg2 - - from: Ghostscript - role: Create outputs preview - - from: ImageMagick - role: Create outputs preview - - from: Git - role: Find origin url -""" -import base64 -import os -import subprocess -import pprint -import re -from shutil import copy2 -from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H -from .error import KiPlotConfigurationError -from .gs import GS -from .optionable import Optionable, BaseOptions -from .kiplot import config_output, get_output_dir, run_command -from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS, read_png, force_list -from .pre_base import BasePreFlight -from .registrable import RegOutput -from .macros import macros, document, output_class # noqa: F401 -from . import log, __version__ - -logger = log.get_logger() -CAT_IMAGE = {'PCB': 'pcbnew', - 'Schematic': 'eeschema', - 'Compress': 'zip', - 'fabrication': 'fabrication', - 'export': 'export', - 'assembly': 'assembly_simple', - 'repair': 'repair', - 'docs': 'project', - 'BoM': 'bom', - '3D': '3d', - 'gerber': 'gerber', - 'drill': 'load_drill', - 'Auxiliar': 'repair'} -EXT_IMAGE = {'gbr': 'file_gbr', - 'gtl': 'file_gbr', - 'gtp': 'file_gbr', - 'gbo': 'file_gbr', - 'gto': 'file_gbr', - 'gbs': 'file_gbr', - 'gbl': 'file_gbr', - 'gts': 'file_gbr', - 'gml': 'file_gbr', - 'gm1': 'file_gbr', - 'gbrjob': 'file_gerber_job', - 'brd': 'file_brd', - 'bz2': 'file_bz2', - 'dxf': 'file_dxf', - 'cad': 'file_cad', - 'drl': 'file_drl', - 'pdf': 'file_pdf', - 'txt': 'file_txt', - 'pos': 'file_pos', - 'csv': 'file_csv', - 'svg': 'file_svg', - 'eps': 'file_eps', - 'png': 'file_png', - 'jpg': 'file_jpg', - 'plt': 'file_plt', - 'ps': 'file_ps', - 'rar': 'file_rar', - 'scad': 'file_scad', - 'stl': 'file_stl', - 'step': 'file_stp', - 'stp': 'file_stp', - 'wrl': 'file_wrl', - 'html': 'file_html', - 'css': 'file_css', - 'xml': 'file_xml', - 'tsv': 'file_tsv', - 'xlsx': 'file_xlsx', - 'xyrs': 'file_xyrs', - 'xz': 'file_xz', - 'gz': 'file_gz', - 'tar': 'file_tar', - 'zip': 'file_zip', - 'kicad_pcb': 'pcbnew', - 'sch': 'eeschema', - 'kicad_sch': 'eeschema', - 'blend': 'file_blend', - 'pcb3d': 'file_pcb3d', - 'json': 'file_json'} -for i in range(31): - n = str(i) - EXT_IMAGE['gl'+n] = 'file_gbr' - EXT_IMAGE['g'+n] = 'file_gbr' - EXT_IMAGE['gp'+n] = 'file_gbr' -CAT_REP = {'PCB': ['pdf_pcb_print', 'svg_pcb_print', 'pcb_print'], - 'Schematic': ['pdf_sch_print', 'svg_sch_print']} -BIG_ICON = 512 -MID_ICON = 64 - -IMAGEABLES_SIMPLE = {'png', 'jpg'} -IMAGEABLES_GS = {'pdf', 'eps', 'ps'} -IMAGEABLES_SVG = {'svg'} -TITLE_HEIGHT = 30 -STYLE = """ - -/* Colors =================================================================== */ - -:root { - --light-bg-color: #ffffff; - --dark-bg-color: #1e1e2f; - --light-bg-color-banner: #dfdfdf; - --dark-bg-color-banner: #27293d; - --light-text-color: #444444; - --dark-text-color: #e5e5e5; - --light-hover-color: #902ec9; - --light-hover-color-act: #652f85; - --dark-hover-color: #ffa500; - --dark-hover-color-act: #cc8400; - --dark-text-color-accent: #a3a3c2; - --light-text-color-accent: #444444; - --light-banner-hover: #b0b0b0; - --dark-banner-hover: #383b4b; - --text-color-accent: #a3a3c2; -} - -/* Main body ================================================================ */ - -body { - margin: 0; - font-family: 'Roboto', sans-serif; - background-color: var(--dark-bg-color); - color: var(--dark-text-color); - transition: - background-color 0.4s ease, - color 0.4s ease, - transition: scrollbar-color 0.2s ease-in-out; -} - -body.dark-mode { - --text-color-accent: var(--dark-text-color-accent); - background-color: var(--dark-bg-color); - color: var(--dark-text-color); -} - -body.light-mode { - --text-color-accent: var(--light-text-color-accent); - background-color: var(--light-bg-color); - color: var(--light-text-color); -} - -/* Top Menu ================================================================= */ - -/* Layout is as follows */ -/* [X/☰] [↩] [↪] (Logo) [☾/☀] [🏠︎] */ - -#topmenu { - position: fixed; - top: 0; - left: 0; - width: 100%; - z-index: 1000; - background-color: var(--dark-bg-color-banner); - padding: 10px 0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); - display: flex; - align-items: center; - justify-content: space-between; - transition: background-color 0.2s ease, color 0.2s ease; -} - -body.light-mode #topmenu { - background-color: var(--light-bg-color-banner); -} - -body.dark-mode #topmenu { - background-color: var(--dark-bg-color-banner); -} - -/* Buttons ================================================================== */ - -/* button corresponds to the navigation buttons (forward, backward, home) */ - -button, #open-sidenav, #close-sidenav { - background: none; - border: none; - color: var(--dark-text-color); - cursor: pointer; - transition: color 0.3s ease; - user-select: none; -} - -body.light-mode #topmenu button, -body.light-mode #topmenu #open-sidenav, -body.light-mode #topmenu #close-sidenav { - color: var(--light-text-color); -} - -body.dark-mode #topmenu button, -body.dark-mode #topmenu #open-sidenav, -body.dark-mode #topmenu #close-sidenav { - color: var(--dark-text-color); -} - -button { - font-size: 20px; - margin: 0 10px; -} - -#open-sidenav, #close-sidenav { - width: 36px; - height: 36px; - line-height: 36px; - text-align: center; - font-size: 28px; - margin-left: 15px; -} - -/* Hover effects */ - -button:hover, #open-sidenav:hover, #close-sidenav:hover { - color: var(--dark-hover-color); -} - -body.dark-mode #topmenu button:hover, -body.dark-mode #topmenu #open-sidenav:hover, -body.dark-mode #topmenu #close-sidenav:hover { - color: var(--dark-hover-color); -} - -body.light-mode #topmenu button:hover, -body.light-mode #topmenu #open-sidenav:hover, -body.light-mode #topmenu #close-sidenav:hover { - color: var(--light-hover-color); -} - -/* Active effects */ - -button:active, #open-sidenav:active, #close-sidenav:active { - color: var(--dark-hover-color-act); - transition: none; -} - -body.dark-mode #topmenu button:active, -body.dark-mode #topmenu #open-sidenav:active, -body.dark-mode #topmenu #close-sidenav:active { - color: var(--dark-hover-color-act); -} - -body.light-mode #topmenu button:active, -body.light-mode #topmenu #open-sidenav:active, -body.light-mode #topmenu #close-sidenav:active { - color: var(--light-hover-color-act); -} - -/* Sidebar Navigation ======================================================= */ - -.sidenav { - position: fixed; - width: 0; /* Initially collapsed */ - height: calc(100% - var(--top-menu-height, 60px)); - top: var(--top-menu-height, 60px); - left: 0; - background-color: #27293d; - overflow-x: hidden; - overflow-y: auto; - transition: - width 0.5s ease, - padding-left 0.5s ease, - scrollbar-color 0.2s ease-in-out, - background-color 0.2s ease-in-out; - box-sizing: border-box; - padding-top: 0; - padding-bottom: 40px; -} - - -body.dark-mode .sidenav { - background-color: var(--dark-bg-color-banner); -} - -body.light-mode .sidenav { - background-color: var(--light-bg-color-banner); -} - -.sidenav > ul:first-child { - margin-top: 20px; /* Padding between top menu and first element of sidenav */ -} - -/* Side Navigation Outputs -------------------------------------------------- */ - -.sidenav-output { - padding: 8px 30px; - text-decoration: none; - font-size: 16px; - color: var(--dark-text-color); - display: block; - transition: color 0.3s ease; - border-radius: 4px; -} - -body.light-mode .sidenav-output { - color: var(--light-text-color); -} - -body.dark-mode .sidenav-output { - color: var(--dark-text-color); -} - -/* Hover effects */ - -.sidenav-output:hover { - color: var(--dark-hover-color); - background-color: var(--dark-banner-hover); -} - -body.dark-mode .sidenav-output:hover { - color: var(--dark-hover-color); - background-color: var(--dark-banner-hover); -} - -body.light-mode .sidenav-output:hover { - color: var(--light-hover-color); - background-color: var(--light-banner-hover); -} - -/* Active effects */ - -.sidenav-output:active { - color: var(--dark-hover-color-act); -} - -body.dark-mode .sidenav-output:active { - color: var(--dark-hover-color-act); -} - -body.light-mode .sidenav-output:active { - color: var(--light-hover-color-act); -} - -/* Side Navigation Categories ----------------------------------------------- */ - -.sidenav-category { - list-style: none; - padding: 0; - margin: 0; - user-select: none; /* Prevent text selection */ -} - -.sidenav-category .folder > span { - display: flex; - align-items: center; - cursor: pointer; - color: var(--dark-text-color-accent); - padding: 10px 20px; - margin-bottom: 0px; - width: 100%; - transition: background-color 0.3s, color 0.3s; - border-radius: 4px; -} - -.sidenav-category .folder-contents { - list-style: none; - margin-left: 20px; - padding: 0; -} - -body.dark-mode .sidenav-category .folder > span { - color: var(--dark-text-color-accent); -} - -body.light-mode .sidenav-category .folder > span { - color: var(--light-text-color); -} - -/* Hover effects */ - -.sidenav-category .folder > span:hover { - background-color: var(--dark-banner-hover); - color: var(--dark-hover-color); -} - -body.dark-mode .sidenav-category .folder > span:hover { - color: var(--dark-hover-color); - background-color: var(--dark-banner-hover); -} - -body.light-mode .sidenav-category .folder > span:hover { - color: var(--light-hover-color); - background-color: var(--light-banner-hover); -} - -/* Active effects */ - -.sidenav-category .folder > span:active { - color: var(--dark-hover-color); - transition: none; -} - -body.dark-mode .sidenav-category .folder > span:active { - color: var(--dark-hover-color-act); -} - -body.light-mode .sidenav-category .folder > span:active { - color: var(--light-hover-color-act); -} - -/* Chevron (arrow) styling -------------------------------------------------- */ - -.chevron { - display: block; - width: 0; - height: 0; - border: 8px solid transparent; - border-left-color: #606077; - margin-right: 8px; - transform-origin: 25% 50%; - transition: transform 0.3s ease, border-left-color 0.3s ease; - pointer-events: none; -} - -body.dark-mode .chevron { - border-left-color: #606077; -} - -body.light-mode .chevron { - border-left-color: #909090; -} - -/* We change styles for when the chevron is pointing down */ - -.folder.open > span .chevron { - border-left-color: var(--dark-text-color-accent); - transform: rotate(90deg); -} - -body.dark-mode .folder.open > span .chevron { - border-left-color: var(--dark-text-color-accent); -} - -body.light-mode .folder.open > span .chevron { - border-left-color: var(--light-text-color-accent); -} - -/* Hover effects */ - -body.dark-mode .folder > span:hover .chevron { - border-left-color: var(--dark-hover-color) -} - -body.light-mode .folder > span:hover .chevron { - border-left-color: var(--light-hover-color) -} - -/* Active effects */ - -body.dark-mode .folder > span:active .chevron { - border-left-color: var(--dark-hover-color-act) -} - - -body.light-mode .folder > span:active .chevron { - border-left-color: var(--light-hover-color-act) -} - -/* Main content ============================================================= */ - -#main { - transition: margin-left 0.5s; - padding: 16px; - margin-top: 80px; -} - -/* Comment field of output is used as a title for each output */ - -.output-comment { - font-size: 1.4em; - font-weight: 500; - color: var(--dark-text-color); - margin: 20px 0 10px 0; - text-align: center; -} - -body.light-mode .output-comment { - color: var(--light-text-color); -} - -body.dark-mode .output-comment { - color: var(--dark-text-color); -} - -/* Kibot version ------------------------------------------------------------ */ - -.generator { - text-align: right; - font-size: 0.6em; - text-decoration: none; -} - -.generator a { - text-decoration: none; /* Removes the underline */ -} - -/* Dark Mode: Regular Text */ -body.dark-mode .generator { - color: var(--dark-text-color-accent); -} - -/* Dark Mode: Hyperlinks */ -body.dark-mode .generator a { - color: var(--dark-hover-color); /* Hyperlink */ -} - -/* Light Mode: Regular Text */ -body.light-mode .generator { - color: var(--light-text-color-accent); -} - -/* Light Mode: Hyperlinks */ -body.light-mode .generator a { - color: var(--light-hover-color); /* Hyperlink */ -} - -/* Category boxes (folder) -------------------------------------------------- */ - -.category-box { - z-index: 1000; - background-color: var(--dark-bg-color-banner); - border: 1px solid var(--dark-bg-color-banner); - border-radius: 8px; - padding: 16px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 400px; - height: 140; - text-decoration: none; - transition: background-color 0.3s ease, transform 0.2s ease; - margin-bottom: 0px; -} - -body.light-mode .category-box { - color: var(--light-text-color); - background-color: var(--light-bg-color-banner); - border: var(--light-bg-color-banner); -} - - -body.dark-mode .category-box { - color: var(--dark-text-color); - background-color: var(--dark-bg-color-banner); - border: var(--dark-bg-color-banner); -} - -.category-box img { - margin-top: 10px; - max-width: 100%; - max-height: 100%; - height: auto; - margin-bottom: 10px; -} - -.category-title { - font-size: 1.4em; - font-weight: 500; - text-align: center; - color: #e5e5e5; - text-decoration: none; - display: inline-block; - margin-top: 0px; - margin-bottom: 0px; -} - -body.light-mode .category-title { - color: var(--light-text-color); -} - -body.dark-mode .category-title { - color: var(--dark-text-color); -} - -/* Hover effects */ - -.category-box:hover { - background-color: var(--dark-banner-hover); - transform: scale(1.05); /* Slight zoom effect */ - cursor: pointer; -} - -body.light-mode .category-box:hover { - background-color: var(--light-banner-hover); -} - -body.dark-mode .category-box:hover { - background-color: var(--dark-banner-hover); -} - -/* Output boxes (files) ----------------------------------------------------- */ - -.output-box { - z-index: 999; - background-color: var(--dark-bg-color-banner); - border: 1px solid var(--dark-bg-color-banner); - border-radius: 8px; - padding: 16px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 300px; - height: 140px; - text-decoration: none; - transition: background-color 0.3s ease, transform 0.2s ease; -} - -/* Offset the scroll position */ -.output-virtual-box { - position: relative; - padding-top: var(--top-menu-height, 80px); - margin-top: calc(-1 * var(--top-menu-height, 80px)); - pointer-events: none; /* Make it non-interactive */ -} - -.output-virtual-box > * { - pointer-events: auto; /* Allow its children to remain interactive */ -} - -/* Some files (e.g. PDF, PNG) have wider output boxes */ - -.output-box.wide { - width: 400px; - height: auto; -} - -.output-box img { - margin-top: 10px; - max-width: 100%; - max-height: 100%; - height: auto; - margin-bottom: 0px; -} - -/* The output boxes are centered and wrap around */ - -.items-container { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 20px; - padding: 20px; -} - -body.light-mode .output-box { - color: var(--light-text-color); - background-color: var(--light-bg-color-banner); - border: var(--light-bg-color-banner); -} - -body.dark-mode .output-box { - color: var(--dark-text-color); - background-color: var(--dark-bg-color-banner); - border: var(--dark-bg-color-banner); -} - -/* Hover effects */ - -.output-box:hover { - background-color: var(--dark-banner-hover); - transform: scale(1.05); - cursor: pointer; -} - -body.light-mode .output-box:hover { - background-color: var(--light-banner-hover); -} - -body.dark-mode .output-box:hover { - background-color: var(--dark-banner-hover); -} - -/* Name of the output below the icon */ - -.output-box .output-name { - color: #8997c6; - font-size: 14px; - margin-top: 0px; - text-align: center; -} - -body.light-mode .output-box .output-name { - color: var(--light-text-color-accent); -} - -body.dark-mode .output-box .output-name { - color: #8997c6; -} - -/* Filename below the icon */ - -.output-box .filename { - text-decoration: none; - color: var(--dark-text-color); - text-align: center; - font-size: 14px; - margin-bottom: 7px; -} - -body.light-mode .output-box .filename { - color: var(--light-text-color); -} - -body.dark-mode .output-box .filename { - color: var(--dark-text-color); -} - -/* Theme Toggle Switch ====================================================== */ - -.theme-switch { - position: relative; - display: inline-block; - width: 50px; - height: 25px; - margin-left: 10px; -} - -/* Hide the default checkbox button */ - -.theme-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.theme-switch span { - position: absolute; - cursor: pointer; - background-color: var(--light-banner-hover); - border-radius: 25px; - top: 0; - left: 0; - right: 0; - bottom: 0; - transition: 0.4s; -} - -.theme-switch span::before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 4px; - bottom: 3px; - background-color: var(--light-bg-color); - border-radius: 50%; - transition: none; /* Disable animation by default */ -} - -.theme-switch span.animate::before { - transition: transform 0.4s ease, background-color 0.4s ease; -} - -.theme-switch input:checked + span { - background-color: var(--dark-bg-color); -} - -.theme-switch input:checked + span::before { - transform: translateX(25px); - background-color: var(--dark-text-color); -} - -/* Scrollbar ================================================================ */ - -body, html { - scroll-behavior: smooth; - scrollbar-width: auto; -} - -body.dark-mode .sidenav { - scrollbar-color: var(--dark-banner-hover) var(--dark-bg-color); -} - -body.light-mode .sidenav { - scrollbar-color: var(--light-banner-hover) var(--light-bg-color); -} - -/* WebKit Scrollbar Styles */ -body::-webkit-scrollbar, .sidenav::-webkit-scrollbar { - width: 12px; - height: 12px; -} - -body::-webkit-scrollbar-thumb, .sidenav::-webkit-scrollbar-thumb { - border-radius: 6px; - background: var(--dark-banner-hover); - border: 2px solid var(--dark-bg-color); -} - -body::-webkit-scrollbar-track, .sidenav::-webkit-scrollbar-track { - border-radius: 6px; - background: var(--dark-bg-color); -} - -body.dark-mode::-webkit-scrollbar-thumb:hover, .sidenav.dark-mode::-webkit-scrollbar-thumb:hover { - background: #44475a !important; -} - -body.light-mode::-webkit-scrollbar-thumb, .sidenav.light-mode::-webkit-scrollbar-thumb { - background: var(--light-banner-hover); - border: 2px solid var(--light-bg-color); -} - -body.light-mode::-webkit-scrollbar-track, .sidenav.light-mode::-webkit-scrollbar-track { - background: var(--light-bg-color); -} - -body.light-mode::-webkit-scrollbar-thumb:hover, .sidenav.light-mode::-webkit-scrollbar-thumb:hover { - background: #909090 !important; -} - -body::-webkit-scrollbar-corner, .sidenav::-webkit-scrollbar-corner { - background: var(--dark-bg-color); -} - -/* Markdown ================================================================= */ - -.markdown-content { - font-family: Roboto, sans-serif; - line-height: 1.6; - padding: 15px; - border-radius: 5px; - max-width: calc(100% - 180px); - white-space: pre-wrap; /* Handle preformatted text */ - transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; -} - -body.light-mode .markdown-content { - background-color: #f9f9f9; - border: 1px solid #ddd; - color: #444444; - transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; -} - -body.dark-mode .markdown-content { - background-color: #1e1e2f; - border: 1px solid #44475a; - color: #e5e5e5; - transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; -} - -/* Tables */ -.markdown-content table { - width: 100%; - border-collapse: collapse; - margin-bottom: 20px; - transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; -} - -body.light-mode .markdown-content table th, -body.light-mode .markdown-content table td { - border: 1px solid #ddd; - padding: 8px; - text-align: left; - background-color: #ffffff; - color: #444444; - transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; -} - -body.dark-mode .markdown-content table th, -body.dark-mode .markdown-content table td { - border: 1px solid #44475a; - padding: 8px; - text-align: left; - background-color: #27293d; - color: #e5e5e5; - transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; -} - -/* Code Blocks */ -.markdown-content pre { - background-color: var(--dark-bg-color-banner); - color: var(--dark-text-color); /* Matches dark theme text */ - padding: 10px; - border-radius: 5px; - overflow-x: auto; - transition: background-color 0.4s ease, color 0.4s ease; -} - -body.light-mode .markdown-content pre { - background-color: var(--light-bg-color-banner); - color: var(--light-text-color); - transition: background-color 0.4s ease, color 0.4s ease; -} - -body.dark-mode .markdown-content pre { - background-color: var(--dark-bg-color-banner); - color: var(--dark-text-color); - transition: background-color 0.4s ease, color 0.4s ease; -} - -/* Inline Code */ -.markdown-content code { - background-color: var(--light-bg-color-banner); - padding: 2px 5px; - border-radius: 3px; - font-family: 'Courier New', Courier, monospace; - transition: background-color 0.4s ease, color 0.4s ease; -} - -body.light-mode .markdown-content code { - background-color: var(--light-bg-color-banner); - color: var(--light-text-color); - transition: background-color 0.4s ease, color 0.4s ease; -} - -body.dark-mode .markdown-content code { - background-color: var(--dark-bg-color-banner); - color: var(--dark-text-color); - transition: background-color 0.4s ease, color 0.4s ease; -} - -/* Links */ -body.light-mode .markdown-content a { - color: var(--light-hover-color); - text-decoration: none; - transition: color 0.4s ease; -} - -body.dark-mode .markdown-content a { - color: var(--dark-hover-color); - text-decoration: none; - transition: color 0.4s ease; -} - -.markdown-content a:hover { - text-decoration: underline; -} - -/* Images */ -.markdown-content img { - max-width: 100%; - height: auto; - display: block; - margin: 10px auto; - transition: opacity 0.4s ease; -} - -.markdown-content pre::-webkit-scrollbar { - height: 12px; /* Horizontal scrollbar height */ -} - -.markdown-content pre::-webkit-scrollbar-thumb { - background: var(--dark-banner-hover); /* Match other scrollbar thumb color */ - border-radius: 6px; /* Round edges */ - border: 2px solid var(--dark-bg-color); /* Outer border matches background */ -} - -.markdown-content pre::-webkit-scrollbar-track { - background: var(--dark-bg-color); /* Match the background color */ - border-radius: 6px; -} - -body.light-mode .markdown-content pre::-webkit-scrollbar-thumb { - background: var(--light-banner-hover); /* Light mode thumb color */ - border: 2px solid var(--light-bg-color); /* Light mode border */ -} - -body.light-mode .markdown-content pre::-webkit-scrollbar-track { - background: var(--light-bg-color); /* Light mode track background */ -} - - -/* Search bar =============================================================== */ - -#search-container, -#search-bar, -#autocomplete-list, -#autocomplete-list li { - transition: background-color 0.3s, color 0.3s, border-color 0.3s; -} - -#search-container { - padding: 10px; - background-color: transparent; - margin-top: 10px; - top: 0; - z-index: 1001; - width: calc(100% - 10px); - box-sizing: border-box; -} - -#search-bar { - width: 100%; /* Match the width of the container */ - padding: 8px; - border: 1px solid var(--light-text-color-accent); - border-radius: 4px; - outline: none; - background-color: transparent; - color: var(--light-text-color); - box-sizing: border-box; /* Ensure padding is included in width */ -} - -#search-bar::placeholder { - color: var(--light-text-color-accent); -} - -#autocomplete-list { - list-style-type: none; - padding: 0; - margin: 5px 0 0; - max-height: 200px; - overflow-y: auto; - background-color: var(--light-bg-color-banner); - border: 1px solid var(--light-text-color-accent); - border-radius: 4px; - position: absolute; - z-index: 1001; - width: auto; /* Width will be dynamically calculated */ - box-sizing: border-box; - display: none; /* Hidden by default */ -} - -#autocomplete-list li { - padding: 8px; - cursor: pointer; - transition: background-color 0.2s; - color: var(--light-text-color); -} - -#autocomplete-list li:hover { - background-color: var(--light-banner-hover); - color: var(--light-hover-color); -} - -.dark-mode #search-bar { - color: var(--dark-text-color); - border-color: var(--dark-text-color-accent); -} - -.dark-mode #search-bar::placeholder { - color: var(--dark-text-color-accent); -} - -.dark-mode #autocomplete-list { - background-color: var(--dark-bg-color-banner); - border-color: var(--dark-text-color-accent); -} - -.dark-mode #autocomplete-list li { - color: var(--dark-text-color); -} - -.dark-mode #autocomplete-list li:hover { - background-color: var(--dark-banner-hover); - color: var(--dark-hover-color); -} - -.highlighted { - background-color: var(--light-banner-hover); /* Same as hover background */ - color: var(--light-hover-color); /* Same as hover text color */ -} - -body.dark-mode .highlighted { - background-color: var(--dark-banner-hover); /* Same as hover background */ - color: var(--dark-hover-color); /* Same as hover text color */ -} - -/* New classes to remove transitions on page load =========================== */ - -body.no-transition, -.no-transition .output-box, -body.no-transition .theme-switch span, -body.no-transition button, -body.no-transition #close-sidenav, -body.no-transition #home-button, -body.no-transition #back-button, -body.no-transition #forward-button, -body.no-transition #topmenu, -body.no-transition .sidenav-category .folder > span, -body.no-transition .sidenav-output, -body.no-transition .category-box, -#search-bar { - transition: none !important; /* Disable transition during page load */ -} - -""" -SCRIPT_NAV_BAR = """ -<script> - -// Side Navigation functions =================================================== - -function openNav() { - const sidenav = document.getElementById("theSideNav"); - const main = document.getElementById("main"); - - sidenav.style.width = "360px"; - sidenav.style.paddingLeft = "20px"; - main.style.marginLeft = "360px"; - document.getElementById("open-sidenav").style.display = "none"; - document.getElementById("close-sidenav").style.display = "inline-block"; -} - -function closeNav() { - const sidenav = document.getElementById("theSideNav"); - const main = document.getElementById("main"); - - sidenav.style.width = "0"; // Close the sidenav - sidenav.style.paddingLeft = "0"; // Reset padding - main.style.marginLeft = "0"; // Reset page content position - document.getElementById("open-sidenav").style.display = "inline-block"; - document.getElementById("close-sidenav").style.display = "none"; -} - -function toggleFolder(folderHeader) { - const folder = folderHeader.parentElement; - const folderContents = folderHeader.nextElementSibling; - - if (folder.classList.contains("open")) { - folder.classList.remove("open"); - folderContents.style.display = "none"; - } else { - folder.classList.add("open"); - folderContents.style.display = "block"; - } - - // Save the updated state - saveSideNavState(); -} - -function saveSideNavState() { - const sidenav = document.getElementById("theSideNav"); - const isOpen = sidenav.style.width !== "0px"; // Check if sidenav is open - - // Save the state of each folder - const folderStates = Array.from(document.querySelectorAll(".folder")).map(folder => ({ - id: folder.querySelector("span").textContent.trim(), // Use folder name as identifier - isOpen: folder.classList.contains("open") // Check if folder is open - })); - - // Save the sidenav and folder states to localStorage - localStorage.setItem("sidenavState", JSON.stringify({ isOpen, folderStates })); -} - -function restoresidenavState() { - const savedState = localStorage.getItem("sidenavState"); - if (savedState) { - const { isOpen, folderStates } = JSON.parse(savedState); - const sidenav = document.getElementById("theSideNav"); - const main = document.getElementById("main"); - - // Temporarily disable animations on page load so elements don't move - sidenav.style.transition = "none"; - main.style.transition = "none"; - const chevrons = document.querySelectorAll(".chevron"); - chevrons.forEach(chevron => { - chevron.style.transition = "none"; - }); - - // Restore side navigation state - if (isOpen) { - openNav() - } else { - closeNav() - } - - // Restore folder open/closed states - folderStates.forEach(({ id, isOpen }) => { - const folder = Array.from(document.querySelectorAll(".folder")) - .find(folder => folder.querySelector("span").textContent.trim() === id); - - if (folder) { - const folderContents = folder.querySelector(".folder-contents"); - if (isOpen) { - folder.classList.add("open"); - folderContents.style.display = "block"; - } else { - folder.classList.remove("open"); - folderContents.style.display = "none"; - } - } - }); - - // Re-enable animation - setTimeout(() => { - sidenav.style.transition = ""; - main.style.transition = ""; - chevrons.forEach(chevron => { - chevron.style.transition = ""; - }); - }, 100); - } -} - -function saveSidenavScrollPosition() { - const sidenav = document.getElementById("theSideNav"); - const scrollPosition = sidenav.scrollTop; - localStorage.setItem("sidenavScrollPosition", scrollPosition); -} - -function restoreSidenavScrollPosition() { - const sidenav = document.getElementById("theSideNav"); - const savedPosition = localStorage.getItem("sidenavScrollPosition"); - if (savedPosition !== null) { - sidenav.scrollTop = parseInt(savedPosition, 10); - } -} - -function adjustSidenavOffset() { - const topMenu = document.getElementById("topmenu"); - const sidenav = document.getElementById("theSideNav"); - - if (topMenu) { - const topMenuHeight = topMenu.offsetHeight; - document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); - } -} - -adjustSidenavOffset(); -window.addEventListener("resize", adjustSidenavOffset); - -/* This is the scrolling offset when we click on an output in the side navigation bar - It should take into account the top menu height */ -function adjustOutputOffset() { - const topMenu = document.getElementById("topmenu"); // Replace with your top menu's ID - if (topMenu) { - const topMenuHeight = topMenu.offsetHeight; // Dynamically get the top menu height - document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); - } -} - -window.addEventListener("DOMContentLoaded", adjustOutputOffset); -window.addEventListener("resize", adjustOutputOffset); - -// Prevent flickering on page navigation -window.addEventListener("beforeunload", () => { - saveSideNavState(); - saveSidenavScrollPosition(); -}); - -window.addEventListener("load", restoreSidenavScrollPosition); -document.addEventListener("DOMContentLoaded", restoresidenavState); - -function initializeSearchBar(searchContainerId, outputSelector) { - const searchContainer = document.getElementById(searchContainerId); - if (!searchContainer) return; // Exit if container is not found - - const searchBar = searchContainer.querySelector("#search-bar"); - const autocompleteList = searchContainer.querySelector("#autocomplete-list"); - const outputLinks = document.querySelectorAll(outputSelector); - - // Collect output names and their hrefs - const outputs = Array.from(outputLinks).map(link => ({ - name: link.textContent.trim(), - href: link.getAttribute("href"), - })); - - let highlightedIndex = -1; // Index of the currently highlighted item - - function adjustAutocompleteWidth() { - const searchBarWidth = searchBar.offsetWidth; - autocompleteList.style.width = `${searchBarWidth}px`; - } - - function updateAutocomplete(query) { - autocompleteList.innerHTML = ""; // Clear suggestions - highlightedIndex = -1; // Reset highlighting - - const matches = outputs.filter(output => - output.name.toLowerCase().includes(query.toLowerCase()) - ); - - matches.forEach(match => { - const listItem = document.createElement("li"); - listItem.textContent = match.name; - listItem.addEventListener("click", () => { - window.location.href = match.href; - }); - autocompleteList.appendChild(listItem); - }); - - autocompleteList.style.display = matches.length ? "block" : "none"; - } - - function highlightItem(index) { - const items = autocompleteList.querySelectorAll("li"); - items.forEach((item, i) => { - if (i === index) { - item.classList.add("highlighted"); - item.scrollIntoView({ block: "nearest" }); - } else { - item.classList.remove("highlighted"); - } - }); - } - - // Add event listeners for search bar input and keydown - searchBar.addEventListener("input", () => { - const query = searchBar.value.trim(); - if (query) updateAutocomplete(query); - else { - autocompleteList.innerHTML = ""; - autocompleteList.style.display = "none"; - } - }); - - searchBar.addEventListener("keydown", (event) => { - const items = autocompleteList.querySelectorAll("li"); - if (!items.length) return; - - if (event.key === "ArrowDown") { - event.preventDefault(); - highlightedIndex = (highlightedIndex + 1) % items.length; - highlightItem(highlightedIndex); - } else if (event.key === "ArrowUp") { - event.preventDefault(); - highlightedIndex = (highlightedIndex - 1 + items.length) % items.length; - highlightItem(highlightedIndex); - } else if (event.key === "Enter" && highlightedIndex >= 0) { - event.preventDefault(); - items[highlightedIndex].click(); - } - }); - - // Hide suggestions when clicking outside - document.addEventListener("click", (event) => { - if (!searchBar.contains(event.target) && !autocompleteList.contains(event.target)) { - autocompleteList.style.display = "none"; - } - }); - - // Adjust width on window resize - adjustAutocompleteWidth(); - window.addEventListener("resize", adjustAutocompleteWidth); -} - -// Initialize search functionality -document.addEventListener("DOMContentLoaded", () => { - initializeSearchBar("search-container", ".sidenav-output"); -}); - -</script> -""" - -SCRIPT = """ -<script> -// Theme toggle ================================================================ - -function toggleTheme() { - const body = document.body; - - // Check if the current theme is dark - const isDark = body.classList.contains('dark-mode'); - - // Toggle between dark and light themes - if (isDark) { - body.classList.remove('dark-mode'); - body.classList.add('light-mode'); - } else { - body.classList.remove('light-mode'); - body.classList.add('dark-mode'); - } - - // Save the selected theme to localStorage - localStorage.setItem('theme', isDark ? 'light' : 'dark'); -} - -// Do not animate theme toggle on page load -document.addEventListener("DOMContentLoaded", () => { - const themeToggle = document.getElementById('themeToggle'); - const toggleSpan = themeToggle.nextElementSibling; // The <span> element - - // Prevent animation on page load - const savedTheme = localStorage.getItem('theme') || 'dark'; - document.body.classList.add(savedTheme === 'dark' ? 'dark-mode' : 'light-mode'); - themeToggle.checked = savedTheme === 'dark'; - - // Add the "animate" class on user interaction - themeToggle.addEventListener('change', () => { - toggleSpan.classList.add('animate'); - setTimeout(() => { - toggleSpan.classList.remove('animate'); // Remove the animation class after completion - }, 400); // Match the CSS transition duration (0.4s) - }); -}); - -// Avoid flickering of theme toggle on page load -document.addEventListener("DOMContentLoaded", () => { - const body = document.body; - - // Temporarily disable transitions during page load - body.classList.add('no-transition'); - - // Remove the no-transition class after the page is fully loaded - setTimeout(() => { - body.classList.remove('no-transition'); - }, 50); // Allow rendering to complete before enabling transitions -}); - -function adjustMainBodyOffset() { - const topMenu = document.getElementById("topmenu"); - const mainBody = document.getElementById("main"); - - if (topMenu && mainBody) { - const topMenuHeight = topMenu.offsetHeight; - mainBody.style.marginTop = `${topMenuHeight}px`; - } -} - -// Apply the adjustment on page load and window resize -window.addEventListener("DOMContentLoaded", adjustMainBodyOffset); -window.addEventListener("resize", adjustMainBodyOffset); -</script> -""" - -SCRIPT_MARKDOWN = """ -<script> -document.addEventListener('DOMContentLoaded', function () { - const md = window.markdownit({ - html: true, - linkify: true, - typographer: true - }); - - // Find all markdown containers and render them - document.querySelectorAll('.markdown-content').forEach(container => { - const rawMarkdown = container.innerHTML; - container.style.display = 'block'; - container.innerHTML = md.render(rawMarkdown); - }); -}); -</script> -""" - - -def _run_command(cmd): - logger.debug('- Executing: '+GS.pasteable_cmd(cmd)) - try: - cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - if e.output: - logger.debug('Output from command: '+e.output.decode()) - logger.non_critical_error(f'Failed to run {cmd[0]}, error {e.returncode}') - return False - if cmd_output.strip(): - logger.debug('- Output from command:\n'+cmd_output.decode()) - return True - - -class Navigate_Results_ModernOptions(BaseOptions): - def __init__(self): - with document: - self.output = GS.def_global_output - """ *Filename for the output (%i=html, %x=navigate) """ - self.link_from_root = '' - """ *The name of a file to create at the main output directory linking to the home page """ - self.skip_not_run = False - """ Skip outputs with `run_by_default: false` """ - self.logo = Optionable - """ [string|boolean=''] PNG file to use as logo, use false to remove. - The KiBot logo is used by default """ - self.logo_url = 'https://github.com/INTI-CMNB/KiBot/' - """ Target link when clicking the logo """ - self.logo_force_height = -1 - """ Force logo height in px. Useful to get consistent heights across different logos. """ - self.title = '' - """ Title for the page, when empty KiBot will try using the schematic or PCB title. - If they are empty the name of the project, schematic or PCB file is used. - You can use %X values and KiCad variables here """ - self.title_url = Optionable - """ [string|boolean=''] Target link when clicking the title, use false to remove. - KiBot will try with the origin of the current git repo when empty """ - self.nav_bar = True - """ Add a side navigation bar to quickly access to the outputs """ - self.render_markdown = True - """ If True, markdown files are rendered; otherwise, they are treated like other files """ - self.display_kibot_version = True - """ If True, display the KiBot version at the bottom of each page """ - self.display_category_images = True - """ If True, we try to display images for categories according to the category type""" - super().__init__() - self._expand_id = 'navigate' - self._expand_ext = 'html' - self._variant_name = self._find_variant_name() - - def config(self, parent): - super().config(parent) - # Logo - if isinstance(self.logo, bool): - self.logo = '' if self.logo else None - elif self.logo: - self.logo = os.path.abspath(self.logo) - if not os.path.isfile(self.logo): - raise KiPlotConfigurationError('Missing logo file `{}`'.format(self.logo)) - try: - self._logo_data, self._logo_w, self._logo_h, _ = read_png(self.logo, logger) - except TypeError as e: - raise KiPlotConfigurationError(f'Only PNG images are supported for the logo ({e})') - if self.logo == '': - # Internal logo - self._logo_w = int(KIBOT_LOGO_W/4) - self._logo_h = int(KIBOT_LOGO_H/4) - self._logo_data = base64.b64decode(KIBOT_LOGO) - elif self.logo is None: - self._logo_w = self._logo_h = 0 - self._logo_data = '' - if self.logo_force_height > 0: - self._logo_w = self.logo_force_height/self._logo_h*self._logo_w - self._logo_h = self.logo_force_height - # Title URL - if isinstance(self.title_url, bool): - self.title_url = '' if self.title_url else None - - def add_to_tree(self, cat, out, o_tree): - if cat == '.' or cat == './': - # Place output directly at the root level - o_tree[out.name] = out - else: - cat = cat.split('/') - node = o_tree - for c in cat: - if c not in node: - # New category - node[c] = {} - node = node[c] - node[out.name] = out - - def svg_to_png(self, svg_file, png_file, width): - cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', png_file, svg_file] - return _run_command(cmd) - - def copy(self, img, width): - """ Copy an SVG icon to the images/ dir. - Tries to convert it to PNG. """ - img_w = "{}_{}".format(os.path.basename(img), width) - if img_w in self.copied_images: - # Already copied, just return its name - return self.copied_images[img_w] - src = os.path.join(self.img_src_dir, img+'.svg') if not img.endswith('.svg') else img - dst = os.path.join(self.out_dir, 'images', img_w) - id = img_w - if self.rsvg_command is not None and self.svg_to_png(src, dst+'.png', width): - img_w += '.png' - else: - copy2(src, dst+'.svg') - img_w += '.svg' - name = os.path.join('images', img_w) - self.copied_images[id] = name - return name - - def can_be_converted(self, ext): - if ext in IMAGEABLES_SVG and self.rsvg_command is None: - logger.warning(W_MISSTOOL+"Missing SVG to PNG converter") - return False - if ext in IMAGEABLES_GS and not self.ps2img_avail: - logger.warning(W_MISSTOOL+"Missing PS/PDF to PNG converter") - return False - if ext in IMAGEABLES_SIMPLE and self.convert_command is None: - logger.warning(W_MISSTOOL+"Missing ImageMagick converter") - return False - return ext in IMAGEABLES_SVG or ext in IMAGEABLES_GS or ext in IMAGEABLES_SIMPLE - - def get_image_for_cat(self, cat, pname): - if self.display_category_images: - img = None - if cat in CAT_REP and self.convert_command is not None: - outs_rep = CAT_REP[cat] - rep_file = None - # Look in all outputs - for o in RegOutput.get_outputs(): - if o.type in outs_rep: - out_dir = get_output_dir(o.dir, o, dry=True) - targets = o.get_targets(out_dir) - for tg in targets: - ext = os.path.splitext(tg)[1][1:].lower() - if os.path.isfile(tg) and self.can_be_converted(ext): - rep_file = tg - break - if rep_file: - break - if rep_file: - cat_img, _ = self.get_image_for_file(rep_file, cat, no_icon=True, is_category=True, category_path=pname) - return cat_img - - if cat in CAT_IMAGE: - img = self.copy(CAT_IMAGE[cat], BIG_ICON) - # Include the category name with the category-title class - cat_img = ( - f'''<div class="category-box" onclick="location.href='{pname}'">''' - f' <img src="{img}" alt="{cat}" width="{BIG_ICON}" height="{BIG_ICON}">' - f' <p class="category-title">{cat}</p>' - f'</div>' - ) - return cat_img - - return (f''' - <div class="category-box" onclick="location.href='{pname}'"> - <p class="category-title">{cat}</p> - </div> - ''') # Fallback if no image - - def compose_image(self, file, ext, img, out_name, no_icon=False): - if not os.path.isfile(file): - logger.warning(W_NOTYET+"{} not yet generated, using an icon".format(os.path.relpath(file))) - return False, None, None - if self.convert_command is None: - return False, None, None - # Create a unique name using the output name and the generated file name - bfname = os.path.splitext(os.path.basename(file))[0] - fname = os.path.join(self.out_dir, 'images', out_name+'_'+bfname+'.png') - # Full path for the icon image - icon = os.path.join(self.out_dir, img) - if ext == 'pdf': - # Only page 1 - file += '[0]' - if ext == 'svg': - tmp_name = GS.tmp_file(suffix='.png') - logger.debug('Temporal convert: {} -> {}'.format(file, tmp_name)) - if not self.svg_to_png(file, tmp_name, BIG_ICON): - return False, None, None - file = tmp_name - cmd = [self.convert_command, file, - # Size for the big icons (width) - '-resize', str(BIG_ICON)+'x'] - if ext == 'ps': - # ImageMagick 6.9.11 (and also the one in Debian 11) rotates the PS - cmd.extend(['-rotate', '90']) - if not no_icon: - cmd.extend([ # Add the file type icon - icon, - # At the bottom right - '-gravity', 'south-east', - # This is a composition, not 2 images - '-composite']) - cmd.append(fname) - res = _run_command(cmd) - if ext == 'svg': - logger.debug('Removing temporal {}'.format(tmp_name)) - os.remove(tmp_name) - return res, fname, os.path.relpath(fname, start=self.out_dir) - - def get_image_for_file(self, file, out_name, no_icon=False, is_category=False, category_path='', image=None): - tg_rel = os.path.relpath(os.path.abspath(file), start=self.out_dir) - ext = os.path.splitext(file)[1][1:].lower() - wide = False - # Copy the icon for this file extension - icon_name = 'folder' if os.path.isdir(file) else EXT_IMAGE.get(ext, 'unknown') - img = self.copy(image or icon_name, MID_ICON) - # Full name for the file - file_full = file - # Just the file, to display it - file = os.path.basename(file) - # The icon size - height = width = MID_ICON - # Check if this file can be represented by an image - if self.can_be_converted(ext): - # Try to compose the image of the file with the icon - ok, fimg, new_img = self.compose_image(file_full, ext, img, 'cat_'+out_name, no_icon) - if ok: - # It was converted, replace the icon by the composited image - img = new_img - # Compute its size - try: - _, width, height, _ = read_png(fimg, logger) - except TypeError: - width = height = 0 - # We are using the big size - wide = True - # Now add the image with its file name as caption - ext_img = '<img src="{}" alt="{}" width="{}" height="{}">'.format(img, file, width, height) - # file = ('<table class="out-img"><tr><td>{}</td></tr><tr><td class="{}">{}</td></tr></table>'. - # format(ext_img, 'td-normal' if no_icon else 'td-small', out_name if no_icon else file)) - - cell_class = "wide" if wide else "" - - # Make the entire output-box clickable - file_name = file - if is_category: - file = f'''\n <div class="category-box" onclick="location.href='{category_path}'">\n''' - else: - file = f'''\n <div class="output-box {cell_class}" onclick="location.href='{tg_rel}'">\n''' - - file += (f''' {ext_img}\n''') - - if is_category: - file += (f''' <p class="category-title">{out_name}</p>\n''') - else: - file += (f''' <p class="filename">{file_name}</p>\n''') - file += (f''' <p class="output-name">{out_name}</p>\n''') - - file += ''' </div>\n''' - - - return file, wide - - def write_kibot_version(self, f): - if self.display_kibot_version: - f.write('<p class="generator">Generated by <a href="https://github.com/INTI-CMNB/KiBot/">KiBot</a> v{}</p>\n'. - format(__version__)) - - def write_head(self, f, title): - f.write('<!DOCTYPE html>\n') - f.write('<html lang="en">\n') - f.write('<head>\n') - f.write(' <title>{}\n'.format(title if title else 'Main page')) - f.write(' \n') # UTF-8 encoding for unicode support - f.write(' \n') - f.write(' \n') - # Include Markdown-it - if self.render_markdown: - f.write(' \n') - f.write('\n') - f.write('\n') - f.write(self.sidenav) - f.write(self.top_menu) - f.write('
    \n') - - def generate_cat_page_for(self, name, node, prev, category): - logger.debug('- Categories: ' + str(node.keys())) - with open(os.path.join(self.out_dir, name), 'wt') as f: - self.write_head(f, category) - name, ext = os.path.splitext(name) - - f.write('
    \n') - # f.write('
    \n') - - for cat, content in node.items(): - if not isinstance(content, dict): - continue - - pname = name + '_' + cat + ext - self.generate_page_for(content, pname, name, category + '/' + cat) - - # f.write('''
    {}
    '''.format(pname, self.get_image_for_cat(cat))) - f.write('''
    {}
    '''.format(self.get_image_for_cat(cat, pname))) - - # f.write('
    \n') - f.write('
    \n') - - # Generate outputs below the categories - self.generate_outputs(f, node) - if self.nav_bar: - f.write(SCRIPT_NAV_BAR) - if self.render_markdown: - f.write(SCRIPT_MARKDOWN) - f.write(SCRIPT) - self.write_kibot_version(f) - f.write('\n\n') - - def adjust_image_paths(self, md_content, current_dir, html_output_dir): - - image_pattern = r'!\[.*?\]\((.*?)\)' # Markdown image paths: ![Alt text](path/to/image.png) - html_img_pattern = r']*src="([^"]+)"' # HTML img src="path/to/image.png" - - def replace_path(match): - original_path = match.group(1) - - # Skip absolute URLs or paths - if original_path.startswith(('http://', 'https://', '/')): - return match.group(0) - - # Convert relative path to absolute and back to relative for the HTML output directory - abs_path = os.path.abspath(os.path.join(current_dir, original_path)) - rel_path = os.path.relpath(abs_path, html_output_dir) - - # Replace the path with the new relative path - return match.group(0).replace(original_path, rel_path) - - # Replace Markdown image paths - md_content = re.sub(image_pattern, replace_path, md_content) - - # Replace HTML tag paths - md_content = re.sub(html_img_pattern, replace_path, md_content) - - return md_content - - def generate_outputs(self, f, node): - """ - Generates the output table dynamically, grouping items into separate categories. - Respects the `render_markdown` option for markdown files. - """ - for oname, out in node.items(): - if isinstance(out, dict): - continue # Skip subcategories here, handled separately - - # Start a container for the category - f.write(f'
    \n') # Virtual box used to hold id - f.write('
    {}
    \n'.format(out.comment or oname)) - - out_dir = get_output_dir(out.dir, out, dry=True) - targets, icons = out.get_navigate_targets(out_dir) - - # Start the items container - f.write('
    \n') - - for tg, icon in zip(targets, icons if icons else [None] * len(targets)): - ext = os.path.splitext(tg)[1].lower() - - if ext == '.md' and self.render_markdown: # Render markdown only if enabled - # Read markdown content - with open(tg, 'r', encoding='utf-8') as md_file: - md_content = md_file.read() - - # Adjust image paths in markdown - md_content = self.adjust_image_paths(md_content, os.path.dirname(tg), self.out_dir) - - # Embed raw markdown into a div - f.write(f''' - - ''') - else: - # Handle other files (icons, images, etc.) - output_cell, wide = self.get_image_for_file(tg, oname, image=icon) - - f.write(output_cell) - - # Close the items container and category box - f.write('
    \n
    \n') - - def generate_end_page_for(self, name, node, prev, category): - logger.debug('- Outputs: '+str(node.keys())) - with open(os.path.join(self.out_dir, name), 'wt') as f: - self.write_head(f, category) - name, ext = os.path.splitext(name) - self.generate_outputs(f, node) - if self.nav_bar: - f.write(SCRIPT_NAV_BAR) - if self.render_markdown: - f.write(SCRIPT_MARKDOWN) - f.write(SCRIPT) - self.write_kibot_version(f) - f.write('\n\n') - - def generate_page_for(self, node, name, prev=None, category=''): - logger.debug('Generating page for ' + name) - self.top_menu = self.generate_top_menu(category) # Update top menu with the current folder name - if isinstance(list(node.values())[0], dict): - self.generate_cat_page_for(name, node, prev, category) - else: - self.generate_end_page_for(name, node, prev, category) - - def get_targets(self, out_dir): - # Listing all targets is too complex, we list the most relevant - # This is good enough to compress the result - name = self._parent.expand_filename(out_dir, self.output) - files = [os.path.join(out_dir, 'images'), - os.path.join(out_dir, 'styles.css'), - os.path.join(out_dir, 'favicon.ico')] - if self.link_from_root: - files.append(os.path.join(GS.out_dir, self.link_from_root)) - self.out_dir = out_dir - self.get_html_names(self.create_tree(), name, files) - return files - - def get_html_names_cat(self, name, node, prev, category, files): - files.append(os.path.join(self.out_dir, name)) - name, ext = os.path.splitext(name) - for cat, content in node.items(): - if not isinstance(content, dict): - continue - pname = name+'_'+cat+ext - self.get_html_names(content, pname, files, name, category+'/'+cat) - - def get_html_names(self, node, name, files, prev=None, category=''): - if isinstance(list(node.values())[0], dict): - self.get_html_names_cat(name, node, prev, category, files) - else: - files.append(os.path.join(self.out_dir, name)) - - def get_html_names_for_path(self, category_path, name, ext): - files = [] - node = self.create_tree() - self.get_html_names_cat(name, node, None, category_path, files) - for file_path in files: - if category_path.replace('/', '_') in file_path: - return os.path.basename(file_path) + ext - return None - - def create_tree(self): - o_tree = {} - BasePreFlight.configure_all() - for n in BasePreFlight.get_in_use_names(): - pre = BasePreFlight.get_preflight(n) - cat = force_list(pre.get_category()) - if not cat: - continue - for c in cat: - self.add_to_tree(c, pre, o_tree) - for o in RegOutput.get_outputs(): - if not o.run_by_default and self.skip_not_run: - # Skip outputs that aren't generated in a regular run - continue - config_output(o) - cat = o.category - if cat is None: - continue - for c in cat: - self.add_to_tree(c, o, o_tree) - return o_tree - - def generate_sidenav_one(self, node, lvl, name, ext): - indent = ' ' * lvl - code = f"{indent}
      \n" - for k, v in node.items(): - if isinstance(v, dict): # Folder (category) - folder_id = f'folder-{name}-{k}'.replace(' ', '-').lower() - code += ( - f"{indent}
    • " - f"" - f" {k}\n" - f"{indent} \n
    • \n" - else: # File (output) - code += f"{indent}
    • {v.name}
    • \n" - code += f"{indent}
    \n" - return code - - def generate_sidenav(self, node, name): - name, ext = os.path.splitext(name) - code = ''' -
    - -
    - -
      -
      - ''' - code += self.generate_sidenav_one(node, 0, name, ext) - code += '
      \n' - return code - - def generate_top_menu(self, category=''): - """ - Generates the top menu with clickable category paths and an up arrow button - that navigates to the root page if the current path is the category name. - """ - fsize = f'{TITLE_HEIGHT}px' - fsize_up = f'{TITLE_HEIGHT+12}px' - small_font_size = f'{int(TITLE_HEIGHT) - 12}px' - smallest_font_size = f'{int(TITLE_HEIGHT) - 16}px' - - code = ''' -
      - ''' - - # Left Section (Buttons + Path) - code += '
      \n' - if self.nav_bar: - code += (f'\n') - code += (f'\n') - code += f'\n' - code += f'\n' - - # Up and Home Buttons - parent_path = '/'.join(category.strip('/').split('/')[:-1]) if category else None - if parent_path is not None: - target_file = self.get_html_names_for_path(parent_path, *os.path.splitext(self.home)) if parent_path else self.home - code += (f'\n') - else: - code += (f'\n') - - code += f'\n' - - # Category Path - if category: - path_parts = category.lstrip('/').split('/') - path_links = [] - current_path = '' - name, ext = os.path.splitext(self.home) - for part in path_parts: - current_path += f'{part}/' - html_file_name = self.get_html_names_for_path(current_path.rstrip('/'), name, ext) - path_links.append(f'{part}') - clickable_path = '/'.join(path_links) - code += f''' - - {clickable_path} - \n''' - - code += '
      \n' - - # Center Section (Title + Company Name) - code += '
      \n' - if self._solved_title: - if self.title_url: - code += f'\n' - code += f'{self._solved_title}\n' - if self.title_url: - code += '\n' - code += f''' -
      - {self._solved_company} -
      - ''' - code += '
      \n' - - # Right Section (Logo, Revision/Variant, Theme Toggle) - code += '
      \n' - code += f''' -
      -
      Rev. {self._solved_revision}
      -
      Variant: {self._variant_name}
      -
      \n''' - if self.logo is not None: - img_name = os.path.join('images', 'logo.png') - if self.logo_url: - code += f'\n' - code += (f'Logo\n') - if self.logo_url: - code += '\n' - - # Move the Theme Toggle Left - code += ''' - - ''' - code += '
      \n' - code += '
      \n' - return code - - def solve_title(self): - base_title = None - if GS.sch: - base_title = GS.sch.get_title() - if GS.board and not base_title: - tb = GS.board.GetTitleBlock() - base_title = tb.GetTitle() - if not base_title: - base_title = GS.pro_basename or GS.sch_basename or GS.pcb_basename or 'Unknown' - text = self.expand_filename_sch(self.title if self.title else '+') - if text[0] == '+': - text = base_title+text[1:] - self._solved_title = text - # Now the URL - if self.title_url is not None and not self.title_url: - # Empty but not None - self._git_command = self.check_tool('Git') - if self._git_command: - res = '' - try: - res = run_command([self._git_command, 'remote', 'get-url', 'origin'], - just_raise=True) - except subprocess.CalledProcessError: - pass - if res: - self.title_url = res - - def solve_revision(self): - base_rev = None - if GS.sch: - GS.load_sch() - GS.load_sch_title_block() - base_rev = GS.sch_rev - if GS.board and not base_rev: - tb = GS.board.GetTitleBlock() - base_rev = tb.GetRevision() - if not base_rev: - base_rev = 'Unknown' - self._solved_revision = base_rev - - def solve_company(self): - base_comp = None - if GS.sch: - GS.load_sch() - GS.load_sch_title_block() - base_comp = GS.sch_comp - if GS.board and not base_comp: - tb = GS.board.GetTitleBlock() - base_comp = tb.GetCompany() - if not base_comp: - base_comp = 'Unknown' - self._solved_company = base_comp - - def run(self, name): - self.out_dir = os.path.dirname(name) - self.img_src_dir = GS.get_resource_path('images') - self.js_src_dir = GS.get_resource_path('navigate_results') - self.img_dst_dir = os.path.join(self.out_dir, 'images') - os.makedirs(self.img_dst_dir, exist_ok=True) - self.copied_images = {} - name = os.path.basename(name) - # Create a tree with all the outputs - o_tree = self.create_tree() - logger.debug('Collected outputs:\n'+pprint.pformat(o_tree)) - if not o_tree: - logger.warning(W_NOOUTPUTS+'No outputs for navigate results') - return - with open(os.path.join(self.out_dir, 'styles.css'), 'wt') as f: - top_margin = TITLE_HEIGHT - f.write(STYLE.replace('@TOP_MAR@', str(top_margin))) - self.rsvg_command = self.check_tool('rsvg1') - self.convert_command = self.check_tool('ImageMagick') - self.ps2img_avail = self.check_tool('Ghostscript') - # Create the pages - self.home = name - copy2(os.path.join(self.img_src_dir, 'favicon.ico'), os.path.join(self.out_dir, 'favicon.ico')) - if self.render_markdown: - copy2(os.path.join(self.js_src_dir, 'markdown-it.min.js'), os.path.join(self.out_dir, 'markdown-it.min.js')) - # Copy the logo image - if self.logo is not None: - with open(os.path.join(self.out_dir, 'images', 'logo.png'), 'wb') as f: - f.write(self._logo_data) - self.solve_title() - self.solve_revision() - self.solve_company() - self.sidenav = self.generate_sidenav(o_tree, name) if self.nav_bar else '' - self.top_menu = self.generate_top_menu() - self.generate_page_for(o_tree, name) - # Link it? - if self.link_from_root: - redir_file = os.path.join(GS.out_dir, self.link_from_root) - rel_start = os.path.relpath(os.path.join(self.out_dir, name), start=GS.out_dir) - logger.debug('Creating redirector: {} -> {}'.format(redir_file, rel_start)) - with open(redir_file, 'wt') as f: - f.write('\n\n'.format(rel_start)) - f.write('\n') - - -@output_class -class Navigate_Results_Modern(BaseOutput): # noqa: F821 - """ Navigate Results - Generates a web page to navigate the generated outputs """ - def __init__(self): - super().__init__() - # Make it low priority so it gets created after all the other outputs - self.priority = 10 - with document: - self.options = Navigate_Results_ModernOptions - """ *[dict={}] Options for the `navigate_results_modern` output """ - # The help is inherited and already mentions the default priority - self.fix_priority_help() - self._any_related = True - - @staticmethod - def get_conf_examples(name, layers): - outs = BaseOutput.simple_conf_examples(name, 'Web page to browse the results', 'Browse') # noqa: F821 - outs[0]['options'] = {'link_from_root': 'index.html', 'skip_not_run': True} - return outs From d14295056a6fe63e7b2cfd5baf47997ebdbe9762 Mon Sep 17 00:00:00 2001 From: Vincent Nguyen Date: Sat, 18 Jan 2025 15:09:59 +0100 Subject: [PATCH 12/14] [Navigate Results RB][Added] New style --- kibot/out_navigate_results_rb.py | 1736 ++++++++++++++++++++++++++++++ 1 file changed, 1736 insertions(+) create mode 100644 kibot/out_navigate_results_rb.py diff --git a/kibot/out_navigate_results_rb.py b/kibot/out_navigate_results_rb.py new file mode 100644 index 000000000..33359dfad --- /dev/null +++ b/kibot/out_navigate_results_rb.py @@ -0,0 +1,1736 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022-2025 Salvador E. Tropea +# Copyright (c) 2022-2025 Nguyen Vincent +# Copyright (c) 2022-2025 Instituto Nacional de Tecnología Industrial +# Contributed by Nguyen Vincent (@nguyen-v) +# License: AGPL-3.0 +# Project: KiBot (formerly KiPlot) +# The Assembly image is a composition from Pixlok and oNline Web Fonts +# The rest are KiCad icons +""" +Dependencies: + - from: RSVG + role: Create outputs preview + id: rsvg1 + - from: RSVG + role: Create PNG icons + id: rsvg2 + - from: Ghostscript + role: Create outputs preview + - from: ImageMagick + role: Create outputs preview + - from: Git + role: Find origin url +""" +import os +from .registrable import RegOutput +from .out_any_navigate_results import Any_Navigate_ResultsOptions, CAT_IMAGE, EXT_IMAGE, CAT_REP +from .macros import macros, document, output_class # noqa: F401 +from .kiplot import get_output_dir +from .misc import read_png +from . import log + +logger = log.get_logger() + +STYLE = """ + +/* Colors =================================================================== */ + +:root { + --light-bg-color: #ffffff; + --dark-bg-color: #1e1e2f; + --light-bg-color-banner: #dfdfdf; + --dark-bg-color-banner: #27293d; + --light-text-color: #444444; + --dark-text-color: #e5e5e5; + --light-hover-color: #902ec9; + --light-hover-color-act: #652f85; + --dark-hover-color: #ffa500; + --dark-hover-color-act: #cc8400; + --dark-text-color-accent: #a3a3c2; + --light-text-color-accent: #444444; + --light-banner-hover: #b0b0b0; + --dark-banner-hover: #383b4b; + --text-color-accent: #a3a3c2; +} + +/* Main body ================================================================ */ + +body { + margin: 0; + font-family: 'Roboto', sans-serif; + background-color: var(--dark-bg-color); + color: var(--dark-text-color); + transition: + background-color 0.4s ease, + color 0.4s ease, + transition: scrollbar-color 0.2s ease-in-out; +} + +body.dark-mode { + --text-color-accent: var(--dark-text-color-accent); + background-color: var(--dark-bg-color); + color: var(--dark-text-color); +} + +body.light-mode { + --text-color-accent: var(--light-text-color-accent); + background-color: var(--light-bg-color); + color: var(--light-text-color); +} + +/* Top Menu ================================================================= */ + +/* Layout is as follows */ +/* [X/☰] [↩] [↪] (Logo) [☾/☀] [🏠︎] */ + +#topmenu { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 1000; + background-color: var(--dark-bg-color-banner); + padding: 10px 0; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); + display: flex; + align-items: center; + justify-content: space-between; + transition: background-color 0.2s ease, color 0.2s ease; +} + +body.light-mode #topmenu { + background-color: var(--light-bg-color-banner); +} + +body.dark-mode #topmenu { + background-color: var(--dark-bg-color-banner); +} + +/* Buttons ================================================================== */ + +/* button corresponds to the navigation buttons (forward, backward, home) */ + +button, #open-navbar, #close-navbar { + background: none; + border: none; + color: var(--dark-text-color); + cursor: pointer; + transition: color 0.3s ease; + user-select: none; +} + +body.light-mode #topmenu button, +body.light-mode #topmenu #open-navbar, +body.light-mode #topmenu #close-navbar { + color: var(--light-text-color); +} + +body.dark-mode #topmenu button, +body.dark-mode #topmenu #open-navbar, +body.dark-mode #topmenu #close-navbar { + color: var(--dark-text-color); +} + +button { + font-size: 20px; + margin: 0 10px; +} + +#open-navbar, #close-navbar { + width: 36px; + height: 36px; + line-height: 36px; + text-align: center; + font-size: 28px; + margin-left: 15px; +} + +/* Hover effects */ + +button:hover, #open-navbar:hover, #close-navbar:hover { + color: var(--dark-hover-color); +} + +body.dark-mode #topmenu button:hover, +body.dark-mode #topmenu #open-navbar:hover, +body.dark-mode #topmenu #close-navbar:hover { + color: var(--dark-hover-color); +} + +body.light-mode #topmenu button:hover, +body.light-mode #topmenu #open-navbar:hover, +body.light-mode #topmenu #close-navbar:hover { + color: var(--light-hover-color); +} + +/* Active effects */ + +button:active, #open-navbar:active, #close-navbar:active { + color: var(--dark-hover-color-act); + transition: none; +} + +body.dark-mode #topmenu button:active, +body.dark-mode #topmenu #open-navbar:active, +body.dark-mode #topmenu #close-navbar:active { + color: var(--dark-hover-color-act); +} + +body.light-mode #topmenu button:active, +body.light-mode #topmenu #open-navbar:active, +body.light-mode #topmenu #close-navbar:active { + color: var(--light-hover-color-act); +} + +/* Sidebar Navigation ======================================================= */ + +.navbar { + position: fixed; + width: 0; /* Initially collapsed */ + height: calc(100% - var(--top-menu-height, 60px)); + top: var(--top-menu-height, 60px); + left: 0; + background-color: #27293d; + overflow-x: hidden; + overflow-y: auto; + transition: + width 0.5s ease, + padding-left 0.5s ease, + scrollbar-color 0.2s ease-in-out, + background-color 0.2s ease-in-out; + box-sizing: border-box; + padding-top: 0; + padding-bottom: 40px; +} + + +body.dark-mode .navbar { + background-color: var(--dark-bg-color-banner); +} + +body.light-mode .navbar { + background-color: var(--light-bg-color-banner); +} + +.navbar > ul:first-child { + margin-top: 20px; /* Padding between top menu and first element of navbar */ +} + +/* Side Navigation Outputs -------------------------------------------------- */ + +.navbar-output { + padding: 8px 30px; + text-decoration: none; + font-size: 16px; + color: var(--dark-text-color); + display: block; + transition: color 0.3s ease; + border-radius: 4px; +} + +body.light-mode .navbar-output { + color: var(--light-text-color); +} + +body.dark-mode .navbar-output { + color: var(--dark-text-color); +} + +/* Hover effects */ + +.navbar-output:hover { + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); +} + +body.dark-mode .navbar-output:hover { + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); +} + +body.light-mode .navbar-output:hover { + color: var(--light-hover-color); + background-color: var(--light-banner-hover); +} + +/* Active effects */ + +.navbar-output:active { + color: var(--dark-hover-color-act); +} + +body.dark-mode .navbar-output:active { + color: var(--dark-hover-color-act); +} + +body.light-mode .navbar-output:active { + color: var(--light-hover-color-act); +} + +/* Side Navigation Categories ----------------------------------------------- */ + +.navbar-category { + list-style: none; + padding: 0; + margin: 0; + user-select: none; /* Prevent text selection */ +} + +.navbar-category .folder > span { + display: flex; + align-items: center; + cursor: pointer; + color: var(--dark-text-color-accent); + padding: 10px 20px; + margin-bottom: 0px; + width: 100%; + transition: background-color 0.3s, color 0.3s; + border-radius: 4px; +} + +.navbar-category .folder-contents { + list-style: none; + margin-left: 20px; + padding: 0; +} + +body.dark-mode .navbar-category .folder > span { + color: var(--dark-text-color-accent); +} + +body.light-mode .navbar-category .folder > span { + color: var(--light-text-color); +} + +/* Hover effects */ + +.navbar-category .folder > span:hover { + background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); +} + +body.dark-mode .navbar-category .folder > span:hover { + color: var(--dark-hover-color); + background-color: var(--dark-banner-hover); +} + +body.light-mode .navbar-category .folder > span:hover { + color: var(--light-hover-color); + background-color: var(--light-banner-hover); +} + +/* Active effects */ + +.navbar-category .folder > span:active { + color: var(--dark-hover-color); + transition: none; +} + +body.dark-mode .navbar-category .folder > span:active { + color: var(--dark-hover-color-act); +} + +body.light-mode .navbar-category .folder > span:active { + color: var(--light-hover-color-act); +} + +/* Chevron (arrow) styling -------------------------------------------------- */ + +.chevron { + display: block; + width: 0; + height: 0; + border: 8px solid transparent; + border-left-color: #606077; + margin-right: 8px; + transform-origin: 25% 50%; + transition: transform 0.3s ease, border-left-color 0.3s ease; + pointer-events: none; +} + +body.dark-mode .chevron { + border-left-color: #606077; +} + +body.light-mode .chevron { + border-left-color: #909090; +} + +/* We change styles for when the chevron is pointing down */ + +.folder.open > span .chevron { + border-left-color: var(--dark-text-color-accent); + transform: rotate(90deg); +} + +body.dark-mode .folder.open > span .chevron { + border-left-color: var(--dark-text-color-accent); +} + +body.light-mode .folder.open > span .chevron { + border-left-color: var(--light-text-color-accent); +} + +/* Hover effects */ + +body.dark-mode .folder > span:hover .chevron { + border-left-color: var(--dark-hover-color) +} + +body.light-mode .folder > span:hover .chevron { + border-left-color: var(--light-hover-color) +} + +/* Active effects */ + +body.dark-mode .folder > span:active .chevron { + border-left-color: var(--dark-hover-color-act) +} + + +body.light-mode .folder > span:active .chevron { + border-left-color: var(--light-hover-color-act) +} + +/* Main content ============================================================= */ + +#main { + transition: margin-left 0.5s; + padding: 16px; + margin-top: 80px; +} + +/* Comment field of output is used as a title for each output */ + +.output-comment { + font-size: 1.4em; + font-weight: 500; + color: var(--dark-text-color); + margin: 20px 0 10px 0; + text-align: center; +} + +body.light-mode .output-comment { + color: var(--light-text-color); +} + +body.dark-mode .output-comment { + color: var(--dark-text-color); +} + +/* Kibot version ------------------------------------------------------------ */ + +.generator { + text-align: right; + font-size: 0.6em; + text-decoration: none; +} + +.generator a { + text-decoration: none; /* Removes the underline */ +} + +/* Dark Mode: Regular Text */ +body.dark-mode .generator { + color: var(--dark-text-color-accent); +} + +/* Dark Mode: Hyperlinks */ +body.dark-mode .generator a { + color: var(--dark-hover-color); /* Hyperlink */ +} + +/* Light Mode: Regular Text */ +body.light-mode .generator { + color: var(--light-text-color-accent); +} + +/* Light Mode: Hyperlinks */ +body.light-mode .generator a { + color: var(--light-hover-color); /* Hyperlink */ +} + +/* Category boxes (folder) -------------------------------------------------- */ + +.category-box { + z-index: 1000; + background-color: var(--dark-bg-color-banner); + border: 1px solid var(--dark-bg-color-banner); + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 400px; + height: 140; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; + margin-bottom: 0px; +} + +body.light-mode .category-box { + color: var(--light-text-color); + background-color: var(--light-bg-color-banner); + border: var(--light-bg-color-banner); +} + + +body.dark-mode .category-box { + color: var(--dark-text-color); + background-color: var(--dark-bg-color-banner); + border: var(--dark-bg-color-banner); +} + +.category-box img { + margin-top: 10px; + max-width: 100%; + max-height: 100%; + height: auto; + margin-bottom: 10px; +} + +.category-title { + font-size: 1.4em; + font-weight: 500; + text-align: center; + color: #e5e5e5; + text-decoration: none; + display: inline-block; + margin-top: 0px; + margin-bottom: 0px; +} + +body.light-mode .category-title { + color: var(--light-text-color); +} + +body.dark-mode .category-title { + color: var(--dark-text-color); +} + +/* Hover effects */ + +.category-box:hover { + background-color: var(--dark-banner-hover); + transform: scale(1.05); /* Slight zoom effect */ + cursor: pointer; +} + +body.light-mode .category-box:hover { + background-color: var(--light-banner-hover); +} + +body.dark-mode .category-box:hover { + background-color: var(--dark-banner-hover); +} + +/* Output boxes (files) ----------------------------------------------------- */ + +.output-box { + z-index: 999; + background-color: var(--dark-bg-color-banner); + border: 1px solid var(--dark-bg-color-banner); + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 300px; + height: 140px; + text-decoration: none; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +/* Offset the scroll position */ +.output-virtual-box { + position: relative; + padding-top: var(--top-menu-height, 80px); + margin-top: calc(-1 * var(--top-menu-height, 80px)); + pointer-events: none; /* Make it non-interactive */ +} + +.output-virtual-box > * { + pointer-events: auto; /* Allow its children to remain interactive */ +} + +/* Some files (e.g. PDF, PNG) have wider output boxes */ + +.output-box.wide { + width: 400px; + height: auto; +} + +.output-box img { + margin-top: 10px; + max-width: 100%; + max-height: 100%; + height: auto; + margin-bottom: 0px; +} + +/* The output boxes are centered and wrap around */ + +.items-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 20px; + padding: 20px; +} + +body.light-mode .output-box { + color: var(--light-text-color); + background-color: var(--light-bg-color-banner); + border: var(--light-bg-color-banner); +} + +body.dark-mode .output-box { + color: var(--dark-text-color); + background-color: var(--dark-bg-color-banner); + border: var(--dark-bg-color-banner); +} + +/* Hover effects */ + +.output-box:hover { + background-color: var(--dark-banner-hover); + transform: scale(1.05); + cursor: pointer; +} + +body.light-mode .output-box:hover { + background-color: var(--light-banner-hover); +} + +body.dark-mode .output-box:hover { + background-color: var(--dark-banner-hover); +} + +/* Name of the output below the icon */ + +.output-box .output-name { + color: #8997c6; + font-size: 14px; + margin-top: 0px; + text-align: center; +} + +body.light-mode .output-box .output-name { + color: var(--light-text-color-accent); +} + +body.dark-mode .output-box .output-name { + color: #8997c6; +} + +/* Filename below the icon */ + +.output-box .filename { + text-decoration: none; + color: var(--dark-text-color); + text-align: center; + font-size: 14px; + margin-bottom: 7px; +} + +body.light-mode .output-box .filename { + color: var(--light-text-color); +} + +body.dark-mode .output-box .filename { + color: var(--dark-text-color); +} + +/* Theme Toggle Switch ====================================================== */ + +.theme-switch { + position: relative; + display: inline-block; + width: 50px; + height: 25px; + margin-left: 10px; +} + +/* Hide the default checkbox button */ + +.theme-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.theme-switch span { + position: absolute; + cursor: pointer; + background-color: var(--light-banner-hover); + border-radius: 25px; + top: 0; + left: 0; + right: 0; + bottom: 0; + transition: 0.4s; +} + +.theme-switch span::before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 4px; + bottom: 3px; + background-color: var(--light-bg-color); + border-radius: 50%; + transition: none; /* Disable animation by default */ +} + +.theme-switch span.animate::before { + transition: transform 0.4s ease, background-color 0.4s ease; +} + +.theme-switch input:checked + span { + background-color: var(--dark-bg-color); +} + +.theme-switch input:checked + span::before { + transform: translateX(25px); + background-color: var(--dark-text-color); +} + +/* Scrollbar ================================================================ */ + +body, html { + scroll-behavior: smooth; + scrollbar-width: auto; +} + +body.dark-mode .navbar { + scrollbar-color: var(--dark-banner-hover) var(--dark-bg-color); +} + +body.light-mode .navbar { + scrollbar-color: var(--light-banner-hover) var(--light-bg-color); +} + +/* WebKit Scrollbar Styles */ +body::-webkit-scrollbar, .navbar::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +body::-webkit-scrollbar-thumb, .navbar::-webkit-scrollbar-thumb { + border-radius: 6px; + background: var(--dark-banner-hover); + border: 2px solid var(--dark-bg-color); +} + +body::-webkit-scrollbar-track, .navbar::-webkit-scrollbar-track { + border-radius: 6px; + background: var(--dark-bg-color); +} + +body.dark-mode::-webkit-scrollbar-thumb:hover, .navbar.dark-mode::-webkit-scrollbar-thumb:hover { + background: #44475a !important; +} + +body.light-mode::-webkit-scrollbar-thumb, .navbar.light-mode::-webkit-scrollbar-thumb { + background: var(--light-banner-hover); + border: 2px solid var(--light-bg-color); +} + +body.light-mode::-webkit-scrollbar-track, .navbar.light-mode::-webkit-scrollbar-track { + background: var(--light-bg-color); +} + +body.light-mode::-webkit-scrollbar-thumb:hover, .navbar.light-mode::-webkit-scrollbar-thumb:hover { + background: #909090 !important; +} + +body::-webkit-scrollbar-corner, .navbar::-webkit-scrollbar-corner { + background: var(--dark-bg-color); +} + +/* Markdown ================================================================= */ + +.markdown-content { + font-family: Roboto, sans-serif; + line-height: 1.6; + padding: 15px; + border-radius: 5px; + max-width: calc(100% - 180px); + white-space: pre-wrap; /* Handle preformatted text */ + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.light-mode .markdown-content { + background-color: #f9f9f9; + border: 1px solid #ddd; + color: #444444; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.dark-mode .markdown-content { + background-color: #1e1e2f; + border: 1px solid #44475a; + color: #e5e5e5; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +/* Tables */ +.markdown-content table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.light-mode .markdown-content table th, +body.light-mode .markdown-content table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; + background-color: #ffffff; + color: #444444; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +body.dark-mode .markdown-content table th, +body.dark-mode .markdown-content table td { + border: 1px solid #44475a; + padding: 8px; + text-align: left; + background-color: #27293d; + color: #e5e5e5; + transition: background-color 0.4s ease, color 0.4s ease, border-color 0.4s ease; +} + +/* Code Blocks */ +.markdown-content pre { + background-color: var(--dark-bg-color-banner); + color: var(--dark-text-color); /* Matches dark theme text */ + padding: 10px; + border-radius: 5px; + overflow-x: auto; + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.light-mode .markdown-content pre { + background-color: var(--light-bg-color-banner); + color: var(--light-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.dark-mode .markdown-content pre { + background-color: var(--dark-bg-color-banner); + color: var(--dark-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +/* Inline Code */ +.markdown-content code { + background-color: var(--light-bg-color-banner); + padding: 2px 5px; + border-radius: 3px; + font-family: 'Courier New', Courier, monospace; + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.light-mode .markdown-content code { + background-color: var(--light-bg-color-banner); + color: var(--light-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +body.dark-mode .markdown-content code { + background-color: var(--dark-bg-color-banner); + color: var(--dark-text-color); + transition: background-color 0.4s ease, color 0.4s ease; +} + +/* Links */ +body.light-mode .markdown-content a { + color: var(--light-hover-color); + text-decoration: none; + transition: color 0.4s ease; +} + +body.dark-mode .markdown-content a { + color: var(--dark-hover-color); + text-decoration: none; + transition: color 0.4s ease; +} + +.markdown-content a:hover { + text-decoration: underline; +} + +/* Images */ +.markdown-content img { + max-width: 100%; + height: auto; + display: block; + margin: 10px auto; + transition: opacity 0.4s ease; +} + +.markdown-content pre::-webkit-scrollbar { + height: 12px; /* Horizontal scrollbar height */ +} + +.markdown-content pre::-webkit-scrollbar-thumb { + background: var(--dark-banner-hover); /* Match other scrollbar thumb color */ + border-radius: 6px; /* Round edges */ + border: 2px solid var(--dark-bg-color); /* Outer border matches background */ +} + +.markdown-content pre::-webkit-scrollbar-track { + background: var(--dark-bg-color); /* Match the background color */ + border-radius: 6px; +} + +body.light-mode .markdown-content pre::-webkit-scrollbar-thumb { + background: var(--light-banner-hover); /* Light mode thumb color */ + border: 2px solid var(--light-bg-color); /* Light mode border */ +} + +body.light-mode .markdown-content pre::-webkit-scrollbar-track { + background: var(--light-bg-color); /* Light mode track background */ +} + + +/* Search bar =============================================================== */ + +#search-container, +#search-bar, +#autocomplete-list, +#autocomplete-list li { + transition: background-color 0.3s, color 0.3s, border-color 0.3s; +} + +#search-container { + padding: 10px; + background-color: transparent; + margin-top: 10px; + top: 0; + z-index: 1001; + width: calc(100% - 10px); + box-sizing: border-box; +} + +#search-bar { + width: 100%; /* Match the width of the container */ + padding: 8px; + border: 1px solid var(--light-text-color-accent); + border-radius: 4px; + outline: none; + background-color: transparent; + color: var(--light-text-color); + box-sizing: border-box; /* Ensure padding is included in width */ +} + +#search-bar::placeholder { + color: var(--light-text-color-accent); +} + +#autocomplete-list { + list-style-type: none; + padding: 0; + margin: 5px 0 0; + max-height: 200px; + overflow-y: auto; + background-color: var(--light-bg-color-banner); + border: 1px solid var(--light-text-color-accent); + border-radius: 4px; + position: absolute; + z-index: 1001; + width: auto; /* Width will be dynamically calculated */ + box-sizing: border-box; + display: none; /* Hidden by default */ +} + +#autocomplete-list li { + padding: 8px; + cursor: pointer; + transition: background-color 0.2s; + color: var(--light-text-color); +} + +#autocomplete-list li:hover { + background-color: var(--light-banner-hover); + color: var(--light-hover-color); +} + +.dark-mode #search-bar { + color: var(--dark-text-color); + border-color: var(--dark-text-color-accent); +} + +.dark-mode #search-bar::placeholder { + color: var(--dark-text-color-accent); +} + +.dark-mode #autocomplete-list { + background-color: var(--dark-bg-color-banner); + border-color: var(--dark-text-color-accent); +} + +.dark-mode #autocomplete-list li { + color: var(--dark-text-color); +} + +.dark-mode #autocomplete-list li:hover { + background-color: var(--dark-banner-hover); + color: var(--dark-hover-color); +} + +.highlighted { + background-color: var(--light-banner-hover); /* Same as hover background */ + color: var(--light-hover-color); /* Same as hover text color */ +} + +body.dark-mode .highlighted { + background-color: var(--dark-banner-hover); /* Same as hover background */ + color: var(--dark-hover-color); /* Same as hover text color */ +} + +/* New classes to remove transitions on page load =========================== */ + +body.no-transition, +.no-transition .output-box, +body.no-transition .theme-switch span, +body.no-transition button, +body.no-transition #close-navbar, +body.no-transition #home-button, +body.no-transition #back-button, +body.no-transition #forward-button, +body.no-transition #topmenu, +body.no-transition .navbar-category .folder > span, +body.no-transition .navbar-output, +body.no-transition .category-box, +#search-bar { + transition: none !important; /* Disable transition during page load */ +} + +""" +SCRIPT_NAV_BAR = """ +<script> + +// Side Navigation functions =================================================== + +function openNav() { + const navbar = document.getElementById("theSideNav"); + const main = document.getElementById("main"); + + navbar.style.width = "360px"; + navbar.style.paddingLeft = "20px"; + main.style.marginLeft = "360px"; + document.getElementById("open-navbar").style.display = "none"; + document.getElementById("close-navbar").style.display = "inline-block"; +} + +function closeNav() { + const navbar = document.getElementById("theSideNav"); + const main = document.getElementById("main"); + + navbar.style.width = "0"; // Close the navbar + navbar.style.paddingLeft = "0"; // Reset padding + main.style.marginLeft = "0"; // Reset page content position + document.getElementById("open-navbar").style.display = "inline-block"; + document.getElementById("close-navbar").style.display = "none"; +} + +function toggleFolder(folderHeader) { + const folder = folderHeader.parentElement; + const folderContents = folderHeader.nextElementSibling; + + if (folder.classList.contains("open")) { + folder.classList.remove("open"); + folderContents.style.display = "none"; + } else { + folder.classList.add("open"); + folderContents.style.display = "block"; + } + + // Save the updated state + saveSideNavState(); +} + +function saveSideNavState() { + const navbar = document.getElementById("theSideNav"); + const isOpen = navbar.style.width !== "0px"; // Check if navbar is open + + // Save the state of each folder + const folderStates = Array.from(document.querySelectorAll(".folder")).map(folder => ({ + id: folder.querySelector("span").textContent.trim(), // Use folder name as identifier + isOpen: folder.classList.contains("open") // Check if folder is open + })); + + // Save the navbar and folder states to localStorage + localStorage.setItem("navbarState", JSON.stringify({ isOpen, folderStates })); +} + +function restorenavbarState() { + const savedState = localStorage.getItem("navbarState"); + if (savedState) { + const { isOpen, folderStates } = JSON.parse(savedState); + const navbar = document.getElementById("theSideNav"); + const main = document.getElementById("main"); + + // Temporarily disable animations on page load so elements don't move + navbar.style.transition = "none"; + main.style.transition = "none"; + const chevrons = document.querySelectorAll(".chevron"); + chevrons.forEach(chevron => { + chevron.style.transition = "none"; + }); + + // Restore side navigation state + if (isOpen) { + openNav() + } else { + closeNav() + } + + // Restore folder open/closed states + folderStates.forEach(({ id, isOpen }) => { + const folder = Array.from(document.querySelectorAll(".folder")) + .find(folder => folder.querySelector("span").textContent.trim() === id); + + if (folder) { + const folderContents = folder.querySelector(".folder-contents"); + if (isOpen) { + folder.classList.add("open"); + folderContents.style.display = "block"; + } else { + folder.classList.remove("open"); + folderContents.style.display = "none"; + } + } + }); + + // Re-enable animation + setTimeout(() => { + navbar.style.transition = ""; + main.style.transition = ""; + chevrons.forEach(chevron => { + chevron.style.transition = ""; + }); + }, 100); + } +} + +function saveSidenavScrollPosition() { + const navbar = document.getElementById("theSideNav"); + const scrollPosition = navbar.scrollTop; + localStorage.setItem("navbarScrollPosition", scrollPosition); +} + +function restoreSidenavScrollPosition() { + const navbar = document.getElementById("theSideNav"); + const savedPosition = localStorage.getItem("navbarScrollPosition"); + if (savedPosition !== null) { + navbar.scrollTop = parseInt(savedPosition, 10); + } +} + +function adjustSidenavOffset() { + const topMenu = document.getElementById("topmenu"); + const navbar = document.getElementById("theSideNav"); + + if (topMenu) { + const topMenuHeight = topMenu.offsetHeight; + document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); + } +} + +adjustSidenavOffset(); +window.addEventListener("resize", adjustSidenavOffset); + +/* This is the scrolling offset when we click on an output in the side navigation bar + It should take into account the top menu height */ +function adjustOutputOffset() { + const topMenu = document.getElementById("topmenu"); // Replace with your top menu's ID + if (topMenu) { + const topMenuHeight = topMenu.offsetHeight; // Dynamically get the top menu height + document.documentElement.style.setProperty('--top-menu-height', `${topMenuHeight}px`); + } +} + +window.addEventListener("DOMContentLoaded", adjustOutputOffset); +window.addEventListener("resize", adjustOutputOffset); + +// Prevent flickering on page navigation +window.addEventListener("beforeunload", () => { + saveSideNavState(); + saveSidenavScrollPosition(); +}); + +window.addEventListener("load", restoreSidenavScrollPosition); +document.addEventListener("DOMContentLoaded", restorenavbarState); + +function initializeSearchBar(searchContainerId, outputSelector) { + const searchContainer = document.getElementById(searchContainerId); + if (!searchContainer) return; // Exit if container is not found + + const searchBar = searchContainer.querySelector("#search-bar"); + const autocompleteList = searchContainer.querySelector("#autocomplete-list"); + const outputLinks = document.querySelectorAll(outputSelector); + + // Collect output names and their hrefs + const outputs = Array.from(outputLinks).map(link => ({ + name: link.textContent.trim(), + href: link.getAttribute("href"), + })); + + let highlightedIndex = -1; // Index of the currently highlighted item + + function adjustAutocompleteWidth() { + const searchBarWidth = searchBar.offsetWidth; + autocompleteList.style.width = `${searchBarWidth}px`; + } + + function updateAutocomplete(query) { + autocompleteList.innerHTML = ""; // Clear suggestions + highlightedIndex = -1; // Reset highlighting + + const matches = outputs.filter(output => + output.name.toLowerCase().includes(query.toLowerCase()) + ); + + matches.forEach(match => { + const listItem = document.createElement("li"); + listItem.textContent = match.name; + listItem.addEventListener("click", () => { + window.location.href = match.href; + }); + autocompleteList.appendChild(listItem); + }); + + autocompleteList.style.display = matches.length ? "block" : "none"; + } + + function highlightItem(index) { + const items = autocompleteList.querySelectorAll("li"); + items.forEach((item, i) => { + if (i === index) { + item.classList.add("highlighted"); + item.scrollIntoView({ block: "nearest" }); + } else { + item.classList.remove("highlighted"); + } + }); + } + + // Add event listeners for search bar input and keydown + searchBar.addEventListener("input", () => { + const query = searchBar.value.trim(); + if (query) updateAutocomplete(query); + else { + autocompleteList.innerHTML = ""; + autocompleteList.style.display = "none"; + } + }); + + searchBar.addEventListener("keydown", (event) => { + const items = autocompleteList.querySelectorAll("li"); + if (!items.length) return; + + if (event.key === "ArrowDown") { + event.preventDefault(); + highlightedIndex = (highlightedIndex + 1) % items.length; + highlightItem(highlightedIndex); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + highlightedIndex = (highlightedIndex - 1 + items.length) % items.length; + highlightItem(highlightedIndex); + } else if (event.key === "Enter" && highlightedIndex >= 0) { + event.preventDefault(); + items[highlightedIndex].click(); + } + }); + + // Hide suggestions when clicking outside + document.addEventListener("click", (event) => { + if (!searchBar.contains(event.target) && !autocompleteList.contains(event.target)) { + autocompleteList.style.display = "none"; + } + }); + + // Adjust width on window resize + adjustAutocompleteWidth(); + window.addEventListener("resize", adjustAutocompleteWidth); +} + +// Initialize search functionality +document.addEventListener("DOMContentLoaded", () => { + initializeSearchBar("search-container", ".navbar-output"); +}); + +</script> +""" + +SCRIPT = """ +<script> +// Theme toggle ================================================================ + +function toggleTheme() { + const body = document.body; + + // Check if the current theme is dark + const isDark = body.classList.contains('dark-mode'); + + // Toggle between dark and light themes + if (isDark) { + body.classList.remove('dark-mode'); + body.classList.add('light-mode'); + } else { + body.classList.remove('light-mode'); + body.classList.add('dark-mode'); + } + + // Save the selected theme to localStorage + localStorage.setItem('theme', isDark ? 'light' : 'dark'); +} + +// Do not animate theme toggle on page load +document.addEventListener("DOMContentLoaded", () => { + const themeToggle = document.getElementById('themeToggle'); + const toggleSpan = themeToggle.nextElementSibling; // The <span> element + + // Prevent animation on page load + const savedTheme = localStorage.getItem('theme') || 'dark'; + document.body.classList.add(savedTheme === 'dark' ? 'dark-mode' : 'light-mode'); + themeToggle.checked = savedTheme === 'dark'; + + // Add the "animate" class on user interaction + themeToggle.addEventListener('change', () => { + toggleSpan.classList.add('animate'); + setTimeout(() => { + toggleSpan.classList.remove('animate'); // Remove the animation class after completion + }, 400); // Match the CSS transition duration (0.4s) + }); +}); + +// Avoid flickering of theme toggle on page load +document.addEventListener("DOMContentLoaded", () => { + const body = document.body; + + // Temporarily disable transitions during page load + body.classList.add('no-transition'); + + // Remove the no-transition class after the page is fully loaded + setTimeout(() => { + body.classList.remove('no-transition'); + }, 50); // Allow rendering to complete before enabling transitions +}); + +function adjustMainBodyOffset() { + const topMenu = document.getElementById("topmenu"); + const mainBody = document.getElementById("main"); + + if (topMenu && mainBody) { + const topMenuHeight = topMenu.offsetHeight; + mainBody.style.marginTop = `${topMenuHeight}px`; + } +} + +// Apply the adjustment on page load and window resize +window.addEventListener("DOMContentLoaded", adjustMainBodyOffset); +window.addEventListener("resize", adjustMainBodyOffset); +</script> +""" + +SCRIPT_MARKDOWN = """ +<script> +document.addEventListener('DOMContentLoaded', function () { + const md = window.markdownit({ + html: true, + linkify: true, + typographer: true + }); + + // Find all markdown containers and render them + document.querySelectorAll('.markdown-content').forEach(container => { + const rawMarkdown = container.innerHTML; + container.style.display = 'block'; + container.innerHTML = md.render(rawMarkdown); + }); +}); +</script> +""" + + +class Navigate_Results_RBOptions(Any_Navigate_ResultsOptions): + def __init__(self): + super().__init__() + self.header = True + self._default_logo_scale = 0.25 + self._style = STYLE + self._big_icon = 512 + + def get_image_for_cat(self, cat, pname): + if self.display_category_images: + img = None + if cat in CAT_REP and self.convert_command is not None: + outs_rep = CAT_REP[cat] + rep_file = None + # Look in all outputs + for o in RegOutput.get_outputs(): + if o.type in outs_rep: + out_dir = get_output_dir(o.dir, o, dry=True) + targets = o.get_targets(out_dir) + for tg in targets: + ext = os.path.splitext(tg)[1][1:].lower() + if os.path.isfile(tg) and self.can_be_converted(ext): + rep_file = tg + break + if rep_file: + break + if rep_file: + cat_img, _ = self.get_image_for_file(rep_file, cat, no_icon=True, is_category=True, category_path=pname) + return cat_img + + if cat in CAT_IMAGE: + img = self.copy(CAT_IMAGE[cat], self._big_icon) + # Include the category name with the category-title class + cat_img = ( + f'''<div class="category-box" onclick="location.href='{pname}'">''' + f' <img src="{img}" alt="{cat}" width="{self._big_icon}" height="{self._big_icon}">' + f' <p class="category-title">{cat}</p>' + f'</div>' + ) + return cat_img + + return (f''' + <div class="category-box" onclick="location.href='{pname}'"> + <p class="category-title">{cat}</p> + </div> + ''') # Fallback if no image + + def get_image_for_file(self, file, out_name, no_icon=False, is_category=False, category_path='', image=None): + tg_rel = os.path.relpath(os.path.abspath(file), start=self.out_dir) + ext = os.path.splitext(file)[1][1:].lower() + wide = False + # Copy the icon for this file extension + icon_name = 'folder' if os.path.isdir(file) else EXT_IMAGE.get(ext, 'unknown') + img = self.copy(image or icon_name, self._mid_icon) + # Full name for the file + file_full = file + # Just the file, to display it + file = os.path.basename(file) + # The icon size + height = width = self._mid_icon + # Check if this file can be represented by an image + if self.can_be_converted(ext): + # Try to compose the image of the file with the icon + ok, fimg, new_img = self.compose_image(file_full, ext, img, 'cat_'+out_name, no_icon) + if ok: + # It was converted, replace the icon by the composited image + img = new_img + # Compute its size + try: + _, width, height, _ = read_png(fimg, logger) + except TypeError: + width = height = 0 + # We are using the big size + wide = True + # Now add the image with its file name as caption + ext_img = '<img src="{}" alt="{}" width="{}" height="{}">'.format(img, file, width, height) + # file = ('<table class="out-img"><tr><td>{}</td></tr><tr><td class="{}">{}</td></tr></table>'. + # format(ext_img, 'td-normal' if no_icon else 'td-small', out_name if no_icon else file)) + + cell_class = "wide" if wide else "" + + # Make the entire output-box clickable + file_name = file + if is_category: + file = f'''\n <div class="category-box" onclick="location.href='{category_path}'">\n''' + else: + file = f'''\n <div class="output-box {cell_class}" onclick="location.href='{tg_rel}'">\n''' + + file += (f''' {ext_img}\n''') + + if is_category: + file += (f''' <p class="category-title">{out_name}</p>\n''') + else: + file += (f''' <p class="filename">{file_name}</p>\n''') + file += (f''' <p class="output-name">{out_name}</p>\n''') + + file += ''' </div>\n''' + return file, wide + + def generate_cat_page_for(self, name, node, prev, category): + logger.debug('- Categories: ' + str(node.keys())) + with open(os.path.join(self.out_dir, name), 'wt') as f: + self.write_head(f, category) + name, ext = os.path.splitext(name) + + f.write('<div class="items-container">\n') + # f.write('<div>\n') + + for cat, content in node.items(): + if not isinstance(content, dict): + continue + + pname = name + '_' + cat + ext + self.generate_page_for(content, pname, name, category + '/' + cat) + + # f.write('''<div onclick="location.href='{}'">{}</div>'''.format(pname, self.get_image_for_cat(cat))) + f.write('''<div>{}</div>'''.format(self.get_image_for_cat(cat, pname))) + + # f.write('</div>\n') + f.write('</div>\n') + + # Generate outputs below the categories + self.generate_outputs(f, node) + if self.nav_bar: + f.write(SCRIPT_NAV_BAR) + if self.render_markdown: + f.write(SCRIPT_MARKDOWN) + f.write(SCRIPT) + self.write_kibot_version(f) + f.write('</body>\n</html>\n') + + def generate_outputs(self, f, node): + for oname, out in node.items(): + if isinstance(out, dict): + continue # Skip subcategories here, handled separately + + # Start a container for the category + f.write(f'<div class="output-virtual-box" id="{oname}">\n') # Virtual box used to hold id + f.write('<div class="output-comment">{}</div>\n'.format(out.comment or oname)) + + out_dir = get_output_dir(out.dir, out, dry=True) + targets, icons = out.get_navigate_targets(out_dir) + + # Start the items container + f.write('<div class="items-container">\n') + + for tg, icon in zip(targets, icons if icons else [None] * len(targets)): + ext = os.path.splitext(tg)[1].lower() + + if ext == '.md' and self.render_markdown: # Render markdown only if enabled + # Read markdown content + with open(tg, 'r', encoding='utf-8') as md_file: + md_content = md_file.read() + + # Adjust image paths in markdown + md_content = self.adjust_image_paths(md_content, os.path.dirname(tg), self.out_dir) + + # Embed raw markdown into a div + f.write(f''' + <div class="markdown-content" style="display: none;">{md_content}</div> + ''') + else: + # Handle other files (icons, images, etc.) + output_cell, wide = self.get_image_for_file(tg, oname, image=icon) + + f.write(output_cell) + + # Close the items container and category box + f.write('</div>\n</div>\n') + + def generate_end_page_for(self, name, node, prev, category): + logger.debug('- Outputs: '+str(node.keys())) + with open(os.path.join(self.out_dir, name), 'wt') as f: + self.write_head(f, category) + name, ext = os.path.splitext(name) + self.generate_outputs(f, node) + if self.nav_bar: + f.write(SCRIPT_NAV_BAR) + if self.render_markdown: + f.write(SCRIPT_MARKDOWN) + f.write(SCRIPT) + self.write_kibot_version(f) + f.write('</body>\n</html>\n') + + def generate_page_for(self, node, name, prev=None, category=''): + logger.debug('Generating page for ' + name) + self.top_menu = self.generate_top_menu(category) # Update top menu with the current folder name + if isinstance(list(node.values())[0], dict): + self.generate_cat_page_for(name, node, prev, category) + else: + self.generate_end_page_for(name, node, prev, category) + + def generate_navbar_one(self, node, lvl, name, ext): + indent = ' ' * lvl + code = f"{indent}<ul class='navbar-category'>\n" + for k, v in node.items(): + if isinstance(v, dict): # Folder (category) + folder_id = f'folder-{name}-{k}'.replace(' ', '-').lower() + code += ( + f"{indent} <li class='folder'>" + f"<span onclick='toggleFolder(this)'>" + f"<span class='chevron'></span> {k}</span>\n" + f"{indent} <ul id='{folder_id}' class='folder-contents' style='display:none;'>\n" + ) + code += self.generate_navbar_one(v, lvl + 1, name + '_' + k, ext) + code += f"{indent} </ul>\n </li>\n" + else: # File (output) + code += f"{indent} <li><a href='{name}{ext}#{v.name}' class='navbar-output'>{v.name}</a></li>\n" + code += f"{indent}</ul>\n" + return code + + def generate_navbar(self, node, name): + name, ext = os.path.splitext(name) + code = ''' + <div id="theSideNav" class="navbar"> + <!-- Search bar container --> + <div id="search-container"> + <input type="text" id="search-bar" placeholder="Search outputs..." autocomplete="off"> + <ul id="autocomplete-list"></ul> + </div> + ''' + code += self.generate_navbar_one(node, 0, name, ext) + code += '</div>\n' + return code + + def generate_top_menu(self, category=''): + """ + Generates the top menu with clickable category paths and an up arrow button + that navigates to the root page if the current path is the category name. + """ + fsize = f'{self._title_height}px' + fsize_up = f'{self._title_height+14}px' + small_font_size = f'{int(self._title_height) - 12}px' + smallest_font_size = f'{int(self._title_height) - 16}px' + + code = ''' + <div id="topmenu" class="topmenu"> + ''' + + # Left Section (Buttons + Path) + code += '<div style="display: flex; align-items: center; flex: 1; min-width: 0; gap: 10px;">\n' + if self.nav_bar: + code += (f'<span id="open-navbar" style="font-size:{fsize};cursor:pointer;" ' + f'onclick="openNav()">☰</span>\n') + code += (f'<span id="close-navbar" style="font-size:{fsize};cursor:pointer;display:none;" ' + f'onclick="closeNav()">⨉</span>\n') + code += f'<button id="back-button" onclick="history.back()" style="font-size:{fsize};">↩</button>\n' + code += f'<button id="forward-button" onclick="history.forward()" style="font-size:{fsize};">↪</button>\n' + + # Up and Home Buttons + parent_path = '/'.join(category.strip('/').split('/')[:-1]) if category else None + if parent_path is not None: + target_file = self.get_html_names_for_path(parent_path, *os.path.splitext(self.home)) if parent_path else self.home + code += (f'<button id="up-button" onclick="location.href=\'{target_file}\'" ' + f'style="font-size:{fsize_up}; position: relative; top: -3px;">⌅</button>\n') + else: + code += ( + f'<button id="up-button" disabled ' + f'style="font-size:{fsize_up}; color: gray; cursor: not-allowed; ' + f'position: relative; top: -3px;">' + '⌅</button>\n' + ) + + code += ( + f'<button id="home-button" onclick="location.href=\'{self.home}\'" ' + f'style="font-size:{self._title_height-5}px; position: relative; top: -2px;">' + '🏠︎</button>\n' + ) + + # Category Path + if category: + path_parts = category.lstrip('/').split('/') + path_links = [] + current_path = '' + name, ext = os.path.splitext(self.home) + for part in path_parts: + current_path += f'{part}/' + html_file_name = self.get_html_names_for_path(current_path.rstrip('/'), name, ext) + path_links.append(f'<a href="{html_file_name}" style="text-decoration:none;color:inherit;">{part}</a>') + clickable_path = '/<wbr>'.join(path_links) + code += f''' + <span style=" + font-size:{small_font_size}; + color: var(--text-color-accent); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + "> + {clickable_path} + </span>\n''' + + code += '</div>\n' + + # Center Section (Title + Company Name) + code += '<div style="text-align: center; flex: 1; min-width: 200px;">\n' + if self._solved_title: + if self.title_url: + code += f'<a href="{self.title_url}" style="text-decoration: none; color: inherit;">\n' + code += f'<span style="font-size:{fsize};">{self._solved_title}</span>\n' + if self.title_url: + code += '</a>\n' + code += f''' + <div style=" + font-size:{smallest_font_size}; + color: var(--text-color-accent); + margin-top: 5px;"> + {self._solved_company} + </div> + ''' + code += '</div>\n' + + # Right Section (Logo, Revision/Variant, Theme Toggle) + code += ( + '<div style="display: flex; align-items: center; flex: 1; ' + 'justify-content: flex-end; min-width: 0; gap: 10px; ' + 'padding-right: 10px;">\n' + ) + code += f''' + <div style=" + text-align: left; + font-size:{smallest_font_size}; + margin-right: 10px; + color: var(--text-color-accent);"> + <div style="margin-bottom: 5px;">Rev. {self._solved_revision}</div> + <div>Variant: {self._variant_name}</div> + </div>\n''' + if self.logo is not None: + img_name = os.path.join('images', 'logo.png') + if self.logo_url: + code += f'<a href="{self.logo_url}" style="margin-right: 10px;">\n' + code += (f'<img src="{img_name}" alt="Logo" ' + f'style="max-height: {str(self._logo_h)}px; max-width: {str(self._logo_w)}px;">\n') + if self.logo_url: + code += '</a>\n' + + # Move the Theme Toggle Left + code += ''' + <label class="theme-switch" style=" + position: relative; + margin-right: 10px; /* Move toggle slightly left */ + max-width: 100%; /* Prevent overflow */ + "> + <input type="checkbox" id="themeToggle" onchange="toggleTheme()"> + <span></span> + </label> + ''' + code += '</div>\n' + code += '</div>\n' + return code + + +@output_class +class Navigate_Results_RB(BaseOutput): # noqa: F821 + """ Navigate Results + Generates a web page to navigate the generated outputs """ + def __init__(self): + super().__init__() + # Make it low priority so it gets created after all the other outputs + self.priority = 10 + with document: + self.options = Navigate_Results_RBOptions + """ *[dict={}] Options for the `navigate_results_rb` output """ + # The help is inherited and already mentions the default priority + self.fix_priority_help() + self._any_related = True + + @staticmethod + def get_conf_examples(name, layers): + outs = BaseOutput.simple_conf_examples(name, 'Web page to browse the results', 'Browse') # noqa: F821 + outs[0]['options'] = {'link_from_root': 'index.html', 'skip_not_run': True} + return outs From 340b84557bdb21a0737d34eddec8c5bdccfe3f5f Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 18 Jan 2025 15:10:34 +0100 Subject: [PATCH 13/14] [Resources][Added] markdown-it JS file --- kibot/resources/navigate_results/markdown-it.min.js | 2 ++ .../navigate_results/markdown-it.min.js:Zone.Identifier | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 kibot/resources/navigate_results/markdown-it.min.js create mode 100644 kibot/resources/navigate_results/markdown-it.min.js:Zone.Identifier diff --git a/kibot/resources/navigate_results/markdown-it.min.js b/kibot/resources/navigate_results/markdown-it.min.js new file mode 100644 index 000000000..5e6f25691 --- /dev/null +++ b/kibot/resources/navigate_results/markdown-it.min.js @@ -0,0 +1,2 @@ +/*! markdown-it 14.1.0 https://github.com/markdown-it/markdown-it @license MIT */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).markdownit=e()}(this,(function(){"use strict";const t={};function e(r,n){"string"!=typeof n&&(n=e.defaultChars);const s=function(e){let r=t[e];if(r)return r;r=t[e]=[];for(let t=0;t<128;t++){const e=String.fromCharCode(t);r.push(e)}for(let t=0;t<e.length;t++){const n=e.charCodeAt(t);r[n]="%"+("0"+n.toString(16).toUpperCase()).slice(-2)}return r}(n);return r.replace(/(%[a-f0-9]{2})+/gi,(function(t){let e="";for(let r=0,n=t.length;r<n;r+=3){const i=parseInt(t.slice(r+1,r+3),16);if(i<128)e+=s[i];else{if(192==(224&i)&&r+3<n){const n=parseInt(t.slice(r+4,r+6),16);if(128==(192&n)){const t=i<<6&1984|63&n;e+=t<128?"\ufffd\ufffd":String.fromCharCode(t),r+=3;continue}}if(224==(240&i)&&r+6<n){const n=parseInt(t.slice(r+4,r+6),16),s=parseInt(t.slice(r+7,r+9),16);if(128==(192&n)&&128==(192&s)){const t=i<<12&61440|n<<6&4032|63&s;e+=t<2048||t>=55296&&t<=57343?"\ufffd\ufffd\ufffd":String.fromCharCode(t),r+=6;continue}}if(240==(248&i)&&r+9<n){const n=parseInt(t.slice(r+4,r+6),16),s=parseInt(t.slice(r+7,r+9),16),o=parseInt(t.slice(r+10,r+12),16);if(128==(192&n)&&128==(192&s)&&128==(192&o)){let t=i<<18&1835008|n<<12&258048|s<<6&4032|63&o;t<65536||t>1114111?e+="\ufffd\ufffd\ufffd\ufffd":(t-=65536,e+=String.fromCharCode(55296+(t>>10),56320+(1023&t))),r+=9;continue}}e+="\ufffd"}}return e}))}e.defaultChars=";/?:@&=+$,#",e.componentChars="";const r={};function n(t,e,s){"string"!=typeof e&&(s=e,e=n.defaultChars),void 0===s&&(s=!0);const i=function(t){let e=r[t];if(e)return e;e=r[t]=[];for(let t=0;t<128;t++){const r=String.fromCharCode(t);/^[0-9a-z]$/i.test(r)?e.push(r):e.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2))}for(let r=0;r<t.length;r++)e[t.charCodeAt(r)]=t[r];return e}(e);let o="";for(let e=0,r=t.length;e<r;e++){const n=t.charCodeAt(e);if(s&&37===n&&e+2<r&&/^[0-9a-f]{2}$/i.test(t.slice(e+1,e+3)))o+=t.slice(e,e+3),e+=2;else if(n<128)o+=i[n];else if(n>=55296&&n<=57343){if(n>=55296&&n<=56319&&e+1<r){const r=t.charCodeAt(e+1);if(r>=56320&&r<=57343){o+=encodeURIComponent(t[e]+t[e+1]),e++;continue}}o+="%EF%BF%BD"}else o+=encodeURIComponent(t[e])}return o}function s(t){let e="";return e+=t.protocol||"",e+=t.slashes?"//":"",e+=t.auth?t.auth+"@":"",t.hostname&&-1!==t.hostname.indexOf(":")?e+="["+t.hostname+"]":e+=t.hostname||"",e+=t.port?":"+t.port:"",e+=t.pathname||"",e+=t.search||"",e+=t.hash||"",e}function i(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}n.defaultChars=";/?:@&=+$,-_.!~*'()#",n.componentChars="-_.!~*'()";const o=/^([a-z0-9.+-]+:)/i,u=/:[0-9]*$/,c=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,a=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),l=["'"].concat(a),h=["%","/","?",";","#"].concat(l),p=["/","?","#"],f=/^[+a-z0-9A-Z_-]{0,63}$/,d=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,_={javascript:!0,"javascript:":!0},m={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function g(t,e){if(t&&t instanceof i)return t;const r=new i;return r.parse(t,e),r}i.prototype.parse=function(t,e){let r,n,s,i=t;if(i=i.trim(),!e&&1===t.split("#").length){const t=c.exec(i);if(t)return this.pathname=t[1],t[2]&&(this.search=t[2]),this}let u=o.exec(i);if(u&&(u=u[0],r=u.toLowerCase(),this.protocol=u,i=i.substr(u.length)),(e||u||i.match(/^\/\/[^@\/]+@[^@\/]+/))&&(s="//"===i.substr(0,2),!s||u&&_[u]||(i=i.substr(2),this.slashes=!0)),!_[u]&&(s||u&&!m[u])){let t,e,r=-1;for(let t=0;t<p.length;t++)n=i.indexOf(p[t]),-1!==n&&(-1===r||n<r)&&(r=n);e=-1===r?i.lastIndexOf("@"):i.lastIndexOf("@",r),-1!==e&&(t=i.slice(0,e),i=i.slice(e+1),this.auth=t),r=-1;for(let t=0;t<h.length;t++)n=i.indexOf(h[t]),-1!==n&&(-1===r||n<r)&&(r=n);-1===r&&(r=i.length),":"===i[r-1]&&r--;const s=i.slice(0,r);i=i.slice(r),this.parseHost(s),this.hostname=this.hostname||"";const o="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!o){const t=this.hostname.split(/\./);for(let e=0,r=t.length;e<r;e++){const r=t[e];if(r&&!r.match(f)){let n="";for(let t=0,e=r.length;t<e;t++)r.charCodeAt(t)>127?n+="x":n+=r[t];if(!n.match(f)){const n=t.slice(0,e),s=t.slice(e+1),o=r.match(d);o&&(n.push(o[1]),s.unshift(o[2])),s.length&&(i=s.join(".")+i),this.hostname=n.join(".");break}}}}this.hostname.length>255&&(this.hostname=""),o&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}const a=i.indexOf("#");-1!==a&&(this.hash=i.substr(a),i=i.slice(0,a));const l=i.indexOf("?");return-1!==l&&(this.search=i.substr(l),i=i.slice(0,l)),i&&(this.pathname=i),m[r]&&this.hostname&&!this.pathname&&(this.pathname=""),this},i.prototype.parseHost=function(t){let e=u.exec(t);e&&(e=e[0],":"!==e&&(this.port=e.substr(1)),t=t.substr(0,t.length-e.length)),t&&(this.hostname=t)};var k,D=Object.freeze({__proto__:null,decode:e,encode:n,format:s,parse:g}),C=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,y=/[\0-\x1F\x7F-\x9F]/,E=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,A=/[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/,b=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/,F=Object.freeze({__proto__:null,Any:C,Cc:y,Cf:/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,P:E,S:A,Z:b}),x=new Uint16Array('\u1d41<\xd5\u0131\u028a\u049d\u057b\u05d0\u0675\u06de\u07a2\u07d6\u080f\u0a4a\u0a91\u0da1\u0e6d\u0f09\u0f26\u10ca\u1228\u12e1\u1415\u149d\u14c3\u14df\u1525\0\0\0\0\0\0\u156b\u16cd\u198d\u1c12\u1ddd\u1f7e\u2060\u21b0\u228d\u23c0\u23fb\u2442\u2824\u2912\u2d08\u2e48\u2fce\u3016\u32ba\u3639\u37ac\u38fe\u3a28\u3a71\u3ae0\u3b2e\u0800EMabcfglmnoprstu\\bfms\x7f\x84\x8b\x90\x95\x98\xa6\xb3\xb9\xc8\xcflig\u803b\xc6\u40c6P\u803b&\u4026cute\u803b\xc1\u40c1reve;\u4102\u0100iyx}rc\u803b\xc2\u40c2;\u4410r;\uc000\ud835\udd04rave\u803b\xc0\u40c0pha;\u4391acr;\u4100d;\u6a53\u0100gp\x9d\xa1on;\u4104f;\uc000\ud835\udd38plyFunction;\u6061ing\u803b\xc5\u40c5\u0100cs\xbe\xc3r;\uc000\ud835\udc9cign;\u6254ilde\u803b\xc3\u40c3ml\u803b\xc4\u40c4\u0400aceforsu\xe5\xfb\xfe\u0117\u011c\u0122\u0127\u012a\u0100cr\xea\xf2kslash;\u6216\u0176\xf6\xf8;\u6ae7ed;\u6306y;\u4411\u0180crt\u0105\u010b\u0114ause;\u6235noullis;\u612ca;\u4392r;\uc000\ud835\udd05pf;\uc000\ud835\udd39eve;\u42d8c\xf2\u0113mpeq;\u624e\u0700HOacdefhilorsu\u014d\u0151\u0156\u0180\u019e\u01a2\u01b5\u01b7\u01ba\u01dc\u0215\u0273\u0278\u027ecy;\u4427PY\u803b\xa9\u40a9\u0180cpy\u015d\u0162\u017aute;\u4106\u0100;i\u0167\u0168\u62d2talDifferentialD;\u6145leys;\u612d\u0200aeio\u0189\u018e\u0194\u0198ron;\u410cdil\u803b\xc7\u40c7rc;\u4108nint;\u6230ot;\u410a\u0100dn\u01a7\u01adilla;\u40b8terDot;\u40b7\xf2\u017fi;\u43a7rcle\u0200DMPT\u01c7\u01cb\u01d1\u01d6ot;\u6299inus;\u6296lus;\u6295imes;\u6297o\u0100cs\u01e2\u01f8kwiseContourIntegral;\u6232eCurly\u0100DQ\u0203\u020foubleQuote;\u601duote;\u6019\u0200lnpu\u021e\u0228\u0247\u0255on\u0100;e\u0225\u0226\u6237;\u6a74\u0180git\u022f\u0236\u023aruent;\u6261nt;\u622fourIntegral;\u622e\u0100fr\u024c\u024e;\u6102oduct;\u6210nterClockwiseContourIntegral;\u6233oss;\u6a2fcr;\uc000\ud835\udc9ep\u0100;C\u0284\u0285\u62d3ap;\u624d\u0580DJSZacefios\u02a0\u02ac\u02b0\u02b4\u02b8\u02cb\u02d7\u02e1\u02e6\u0333\u048d\u0100;o\u0179\u02a5trahd;\u6911cy;\u4402cy;\u4405cy;\u440f\u0180grs\u02bf\u02c4\u02c7ger;\u6021r;\u61a1hv;\u6ae4\u0100ay\u02d0\u02d5ron;\u410e;\u4414l\u0100;t\u02dd\u02de\u6207a;\u4394r;\uc000\ud835\udd07\u0100af\u02eb\u0327\u0100cm\u02f0\u0322ritical\u0200ADGT\u0300\u0306\u0316\u031ccute;\u40b4o\u0174\u030b\u030d;\u42d9bleAcute;\u42ddrave;\u4060ilde;\u42dcond;\u62c4ferentialD;\u6146\u0470\u033d\0\0\0\u0342\u0354\0\u0405f;\uc000\ud835\udd3b\u0180;DE\u0348\u0349\u034d\u40a8ot;\u60dcqual;\u6250ble\u0300CDLRUV\u0363\u0372\u0382\u03cf\u03e2\u03f8ontourIntegra\xec\u0239o\u0274\u0379\0\0\u037b\xbb\u0349nArrow;\u61d3\u0100eo\u0387\u03a4ft\u0180ART\u0390\u0396\u03a1rrow;\u61d0ightArrow;\u61d4e\xe5\u02cang\u0100LR\u03ab\u03c4eft\u0100AR\u03b3\u03b9rrow;\u67f8ightArrow;\u67faightArrow;\u67f9ight\u0100AT\u03d8\u03derrow;\u61d2ee;\u62a8p\u0241\u03e9\0\0\u03efrrow;\u61d1ownArrow;\u61d5erticalBar;\u6225n\u0300ABLRTa\u0412\u042a\u0430\u045e\u047f\u037crrow\u0180;BU\u041d\u041e\u0422\u6193ar;\u6913pArrow;\u61f5reve;\u4311eft\u02d2\u043a\0\u0446\0\u0450ightVector;\u6950eeVector;\u695eector\u0100;B\u0459\u045a\u61bdar;\u6956ight\u01d4\u0467\0\u0471eeVector;\u695fector\u0100;B\u047a\u047b\u61c1ar;\u6957ee\u0100;A\u0486\u0487\u62a4rrow;\u61a7\u0100ct\u0492\u0497r;\uc000\ud835\udc9frok;\u4110\u0800NTacdfglmopqstux\u04bd\u04c0\u04c4\u04cb\u04de\u04e2\u04e7\u04ee\u04f5\u0521\u052f\u0536\u0552\u055d\u0560\u0565G;\u414aH\u803b\xd0\u40d0cute\u803b\xc9\u40c9\u0180aiy\u04d2\u04d7\u04dcron;\u411arc\u803b\xca\u40ca;\u442dot;\u4116r;\uc000\ud835\udd08rave\u803b\xc8\u40c8ement;\u6208\u0100ap\u04fa\u04fecr;\u4112ty\u0253\u0506\0\0\u0512mallSquare;\u65fberySmallSquare;\u65ab\u0100gp\u0526\u052aon;\u4118f;\uc000\ud835\udd3csilon;\u4395u\u0100ai\u053c\u0549l\u0100;T\u0542\u0543\u6a75ilde;\u6242librium;\u61cc\u0100ci\u0557\u055ar;\u6130m;\u6a73a;\u4397ml\u803b\xcb\u40cb\u0100ip\u056a\u056fsts;\u6203onentialE;\u6147\u0280cfios\u0585\u0588\u058d\u05b2\u05ccy;\u4424r;\uc000\ud835\udd09lled\u0253\u0597\0\0\u05a3mallSquare;\u65fcerySmallSquare;\u65aa\u0370\u05ba\0\u05bf\0\0\u05c4f;\uc000\ud835\udd3dAll;\u6200riertrf;\u6131c\xf2\u05cb\u0600JTabcdfgorst\u05e8\u05ec\u05ef\u05fa\u0600\u0612\u0616\u061b\u061d\u0623\u066c\u0672cy;\u4403\u803b>\u403emma\u0100;d\u05f7\u05f8\u4393;\u43dcreve;\u411e\u0180eiy\u0607\u060c\u0610dil;\u4122rc;\u411c;\u4413ot;\u4120r;\uc000\ud835\udd0a;\u62d9pf;\uc000\ud835\udd3eeater\u0300EFGLST\u0635\u0644\u064e\u0656\u065b\u0666qual\u0100;L\u063e\u063f\u6265ess;\u62dbullEqual;\u6267reater;\u6aa2ess;\u6277lantEqual;\u6a7eilde;\u6273cr;\uc000\ud835\udca2;\u626b\u0400Aacfiosu\u0685\u068b\u0696\u069b\u069e\u06aa\u06be\u06caRDcy;\u442a\u0100ct\u0690\u0694ek;\u42c7;\u405eirc;\u4124r;\u610clbertSpace;\u610b\u01f0\u06af\0\u06b2f;\u610dizontalLine;\u6500\u0100ct\u06c3\u06c5\xf2\u06a9rok;\u4126mp\u0144\u06d0\u06d8ownHum\xf0\u012fqual;\u624f\u0700EJOacdfgmnostu\u06fa\u06fe\u0703\u0707\u070e\u071a\u071e\u0721\u0728\u0744\u0778\u078b\u078f\u0795cy;\u4415lig;\u4132cy;\u4401cute\u803b\xcd\u40cd\u0100iy\u0713\u0718rc\u803b\xce\u40ce;\u4418ot;\u4130r;\u6111rave\u803b\xcc\u40cc\u0180;ap\u0720\u072f\u073f\u0100cg\u0734\u0737r;\u412ainaryI;\u6148lie\xf3\u03dd\u01f4\u0749\0\u0762\u0100;e\u074d\u074e\u622c\u0100gr\u0753\u0758ral;\u622bsection;\u62c2isible\u0100CT\u076c\u0772omma;\u6063imes;\u6062\u0180gpt\u077f\u0783\u0788on;\u412ef;\uc000\ud835\udd40a;\u4399cr;\u6110ilde;\u4128\u01eb\u079a\0\u079ecy;\u4406l\u803b\xcf\u40cf\u0280cfosu\u07ac\u07b7\u07bc\u07c2\u07d0\u0100iy\u07b1\u07b5rc;\u4134;\u4419r;\uc000\ud835\udd0dpf;\uc000\ud835\udd41\u01e3\u07c7\0\u07ccr;\uc000\ud835\udca5rcy;\u4408kcy;\u4404\u0380HJacfos\u07e4\u07e8\u07ec\u07f1\u07fd\u0802\u0808cy;\u4425cy;\u440cppa;\u439a\u0100ey\u07f6\u07fbdil;\u4136;\u441ar;\uc000\ud835\udd0epf;\uc000\ud835\udd42cr;\uc000\ud835\udca6\u0580JTaceflmost\u0825\u0829\u082c\u0850\u0863\u09b3\u09b8\u09c7\u09cd\u0a37\u0a47cy;\u4409\u803b<\u403c\u0280cmnpr\u0837\u083c\u0841\u0844\u084dute;\u4139bda;\u439bg;\u67ealacetrf;\u6112r;\u619e\u0180aey\u0857\u085c\u0861ron;\u413ddil;\u413b;\u441b\u0100fs\u0868\u0970t\u0500ACDFRTUVar\u087e\u08a9\u08b1\u08e0\u08e6\u08fc\u092f\u095b\u0390\u096a\u0100nr\u0883\u088fgleBracket;\u67e8row\u0180;BR\u0899\u089a\u089e\u6190ar;\u61e4ightArrow;\u61c6eiling;\u6308o\u01f5\u08b7\0\u08c3bleBracket;\u67e6n\u01d4\u08c8\0\u08d2eeVector;\u6961ector\u0100;B\u08db\u08dc\u61c3ar;\u6959loor;\u630aight\u0100AV\u08ef\u08f5rrow;\u6194ector;\u694e\u0100er\u0901\u0917e\u0180;AV\u0909\u090a\u0910\u62a3rrow;\u61a4ector;\u695aiangle\u0180;BE\u0924\u0925\u0929\u62b2ar;\u69cfqual;\u62b4p\u0180DTV\u0937\u0942\u094cownVector;\u6951eeVector;\u6960ector\u0100;B\u0956\u0957\u61bfar;\u6958ector\u0100;B\u0965\u0966\u61bcar;\u6952ight\xe1\u039cs\u0300EFGLST\u097e\u098b\u0995\u099d\u09a2\u09adqualGreater;\u62daullEqual;\u6266reater;\u6276ess;\u6aa1lantEqual;\u6a7dilde;\u6272r;\uc000\ud835\udd0f\u0100;e\u09bd\u09be\u62d8ftarrow;\u61daidot;\u413f\u0180npw\u09d4\u0a16\u0a1bg\u0200LRlr\u09de\u09f7\u0a02\u0a10eft\u0100AR\u09e6\u09ecrrow;\u67f5ightArrow;\u67f7ightArrow;\u67f6eft\u0100ar\u03b3\u0a0aight\xe1\u03bfight\xe1\u03caf;\uc000\ud835\udd43er\u0100LR\u0a22\u0a2ceftArrow;\u6199ightArrow;\u6198\u0180cht\u0a3e\u0a40\u0a42\xf2\u084c;\u61b0rok;\u4141;\u626a\u0400acefiosu\u0a5a\u0a5d\u0a60\u0a77\u0a7c\u0a85\u0a8b\u0a8ep;\u6905y;\u441c\u0100dl\u0a65\u0a6fiumSpace;\u605flintrf;\u6133r;\uc000\ud835\udd10nusPlus;\u6213pf;\uc000\ud835\udd44c\xf2\u0a76;\u439c\u0480Jacefostu\u0aa3\u0aa7\u0aad\u0ac0\u0b14\u0b19\u0d91\u0d97\u0d9ecy;\u440acute;\u4143\u0180aey\u0ab4\u0ab9\u0aberon;\u4147dil;\u4145;\u441d\u0180gsw\u0ac7\u0af0\u0b0eative\u0180MTV\u0ad3\u0adf\u0ae8ediumSpace;\u600bhi\u0100cn\u0ae6\u0ad8\xeb\u0ad9eryThi\xee\u0ad9ted\u0100GL\u0af8\u0b06reaterGreate\xf2\u0673essLes\xf3\u0a48Line;\u400ar;\uc000\ud835\udd11\u0200Bnpt\u0b22\u0b28\u0b37\u0b3areak;\u6060BreakingSpace;\u40a0f;\u6115\u0680;CDEGHLNPRSTV\u0b55\u0b56\u0b6a\u0b7c\u0ba1\u0beb\u0c04\u0c5e\u0c84\u0ca6\u0cd8\u0d61\u0d85\u6aec\u0100ou\u0b5b\u0b64ngruent;\u6262pCap;\u626doubleVerticalBar;\u6226\u0180lqx\u0b83\u0b8a\u0b9bement;\u6209ual\u0100;T\u0b92\u0b93\u6260ilde;\uc000\u2242\u0338ists;\u6204reater\u0380;EFGLST\u0bb6\u0bb7\u0bbd\u0bc9\u0bd3\u0bd8\u0be5\u626fqual;\u6271ullEqual;\uc000\u2267\u0338reater;\uc000\u226b\u0338ess;\u6279lantEqual;\uc000\u2a7e\u0338ilde;\u6275ump\u0144\u0bf2\u0bfdownHump;\uc000\u224e\u0338qual;\uc000\u224f\u0338e\u0100fs\u0c0a\u0c27tTriangle\u0180;BE\u0c1a\u0c1b\u0c21\u62eaar;\uc000\u29cf\u0338qual;\u62ecs\u0300;EGLST\u0c35\u0c36\u0c3c\u0c44\u0c4b\u0c58\u626equal;\u6270reater;\u6278ess;\uc000\u226a\u0338lantEqual;\uc000\u2a7d\u0338ilde;\u6274ested\u0100GL\u0c68\u0c79reaterGreater;\uc000\u2aa2\u0338essLess;\uc000\u2aa1\u0338recedes\u0180;ES\u0c92\u0c93\u0c9b\u6280qual;\uc000\u2aaf\u0338lantEqual;\u62e0\u0100ei\u0cab\u0cb9verseElement;\u620cghtTriangle\u0180;BE\u0ccb\u0ccc\u0cd2\u62ebar;\uc000\u29d0\u0338qual;\u62ed\u0100qu\u0cdd\u0d0cuareSu\u0100bp\u0ce8\u0cf9set\u0100;E\u0cf0\u0cf3\uc000\u228f\u0338qual;\u62e2erset\u0100;E\u0d03\u0d06\uc000\u2290\u0338qual;\u62e3\u0180bcp\u0d13\u0d24\u0d4eset\u0100;E\u0d1b\u0d1e\uc000\u2282\u20d2qual;\u6288ceeds\u0200;EST\u0d32\u0d33\u0d3b\u0d46\u6281qual;\uc000\u2ab0\u0338lantEqual;\u62e1ilde;\uc000\u227f\u0338erset\u0100;E\u0d58\u0d5b\uc000\u2283\u20d2qual;\u6289ilde\u0200;EFT\u0d6e\u0d6f\u0d75\u0d7f\u6241qual;\u6244ullEqual;\u6247ilde;\u6249erticalBar;\u6224cr;\uc000\ud835\udca9ilde\u803b\xd1\u40d1;\u439d\u0700Eacdfgmoprstuv\u0dbd\u0dc2\u0dc9\u0dd5\u0ddb\u0de0\u0de7\u0dfc\u0e02\u0e20\u0e22\u0e32\u0e3f\u0e44lig;\u4152cute\u803b\xd3\u40d3\u0100iy\u0dce\u0dd3rc\u803b\xd4\u40d4;\u441eblac;\u4150r;\uc000\ud835\udd12rave\u803b\xd2\u40d2\u0180aei\u0dee\u0df2\u0df6cr;\u414cga;\u43a9cron;\u439fpf;\uc000\ud835\udd46enCurly\u0100DQ\u0e0e\u0e1aoubleQuote;\u601cuote;\u6018;\u6a54\u0100cl\u0e27\u0e2cr;\uc000\ud835\udcaaash\u803b\xd8\u40d8i\u016c\u0e37\u0e3cde\u803b\xd5\u40d5es;\u6a37ml\u803b\xd6\u40d6er\u0100BP\u0e4b\u0e60\u0100ar\u0e50\u0e53r;\u603eac\u0100ek\u0e5a\u0e5c;\u63deet;\u63b4arenthesis;\u63dc\u0480acfhilors\u0e7f\u0e87\u0e8a\u0e8f\u0e92\u0e94\u0e9d\u0eb0\u0efcrtialD;\u6202y;\u441fr;\uc000\ud835\udd13i;\u43a6;\u43a0usMinus;\u40b1\u0100ip\u0ea2\u0eadncareplan\xe5\u069df;\u6119\u0200;eio\u0eb9\u0eba\u0ee0\u0ee4\u6abbcedes\u0200;EST\u0ec8\u0ec9\u0ecf\u0eda\u627aqual;\u6aaflantEqual;\u627cilde;\u627eme;\u6033\u0100dp\u0ee9\u0eeeuct;\u620fortion\u0100;a\u0225\u0ef9l;\u621d\u0100ci\u0f01\u0f06r;\uc000\ud835\udcab;\u43a8\u0200Ufos\u0f11\u0f16\u0f1b\u0f1fOT\u803b"\u4022r;\uc000\ud835\udd14pf;\u611acr;\uc000\ud835\udcac\u0600BEacefhiorsu\u0f3e\u0f43\u0f47\u0f60\u0f73\u0fa7\u0faa\u0fad\u1096\u10a9\u10b4\u10bearr;\u6910G\u803b\xae\u40ae\u0180cnr\u0f4e\u0f53\u0f56ute;\u4154g;\u67ebr\u0100;t\u0f5c\u0f5d\u61a0l;\u6916\u0180aey\u0f67\u0f6c\u0f71ron;\u4158dil;\u4156;\u4420\u0100;v\u0f78\u0f79\u611cerse\u0100EU\u0f82\u0f99\u0100lq\u0f87\u0f8eement;\u620builibrium;\u61cbpEquilibrium;\u696fr\xbb\u0f79o;\u43a1ght\u0400ACDFTUVa\u0fc1\u0feb\u0ff3\u1022\u1028\u105b\u1087\u03d8\u0100nr\u0fc6\u0fd2gleBracket;\u67e9row\u0180;BL\u0fdc\u0fdd\u0fe1\u6192ar;\u61e5eftArrow;\u61c4eiling;\u6309o\u01f5\u0ff9\0\u1005bleBracket;\u67e7n\u01d4\u100a\0\u1014eeVector;\u695dector\u0100;B\u101d\u101e\u61c2ar;\u6955loor;\u630b\u0100er\u102d\u1043e\u0180;AV\u1035\u1036\u103c\u62a2rrow;\u61a6ector;\u695biangle\u0180;BE\u1050\u1051\u1055\u62b3ar;\u69d0qual;\u62b5p\u0180DTV\u1063\u106e\u1078ownVector;\u694feeVector;\u695cector\u0100;B\u1082\u1083\u61bear;\u6954ector\u0100;B\u1091\u1092\u61c0ar;\u6953\u0100pu\u109b\u109ef;\u611dndImplies;\u6970ightarrow;\u61db\u0100ch\u10b9\u10bcr;\u611b;\u61b1leDelayed;\u69f4\u0680HOacfhimoqstu\u10e4\u10f1\u10f7\u10fd\u1119\u111e\u1151\u1156\u1161\u1167\u11b5\u11bb\u11bf\u0100Cc\u10e9\u10eeHcy;\u4429y;\u4428FTcy;\u442ccute;\u415a\u0280;aeiy\u1108\u1109\u110e\u1113\u1117\u6abcron;\u4160dil;\u415erc;\u415c;\u4421r;\uc000\ud835\udd16ort\u0200DLRU\u112a\u1134\u113e\u1149ownArrow\xbb\u041eeftArrow\xbb\u089aightArrow\xbb\u0fddpArrow;\u6191gma;\u43a3allCircle;\u6218pf;\uc000\ud835\udd4a\u0272\u116d\0\0\u1170t;\u621aare\u0200;ISU\u117b\u117c\u1189\u11af\u65a1ntersection;\u6293u\u0100bp\u118f\u119eset\u0100;E\u1197\u1198\u628fqual;\u6291erset\u0100;E\u11a8\u11a9\u6290qual;\u6292nion;\u6294cr;\uc000\ud835\udcaear;\u62c6\u0200bcmp\u11c8\u11db\u1209\u120b\u0100;s\u11cd\u11ce\u62d0et\u0100;E\u11cd\u11d5qual;\u6286\u0100ch\u11e0\u1205eeds\u0200;EST\u11ed\u11ee\u11f4\u11ff\u627bqual;\u6ab0lantEqual;\u627dilde;\u627fTh\xe1\u0f8c;\u6211\u0180;es\u1212\u1213\u1223\u62d1rset\u0100;E\u121c\u121d\u6283qual;\u6287et\xbb\u1213\u0580HRSacfhiors\u123e\u1244\u1249\u1255\u125e\u1271\u1276\u129f\u12c2\u12c8\u12d1ORN\u803b\xde\u40deADE;\u6122\u0100Hc\u124e\u1252cy;\u440by;\u4426\u0100bu\u125a\u125c;\u4009;\u43a4\u0180aey\u1265\u126a\u126fron;\u4164dil;\u4162;\u4422r;\uc000\ud835\udd17\u0100ei\u127b\u1289\u01f2\u1280\0\u1287efore;\u6234a;\u4398\u0100cn\u128e\u1298kSpace;\uc000\u205f\u200aSpace;\u6009lde\u0200;EFT\u12ab\u12ac\u12b2\u12bc\u623cqual;\u6243ullEqual;\u6245ilde;\u6248pf;\uc000\ud835\udd4bipleDot;\u60db\u0100ct\u12d6\u12dbr;\uc000\ud835\udcafrok;\u4166\u0ae1\u12f7\u130e\u131a\u1326\0\u132c\u1331\0\0\0\0\0\u1338\u133d\u1377\u1385\0\u13ff\u1404\u140a\u1410\u0100cr\u12fb\u1301ute\u803b\xda\u40dar\u0100;o\u1307\u1308\u619fcir;\u6949r\u01e3\u1313\0\u1316y;\u440eve;\u416c\u0100iy\u131e\u1323rc\u803b\xdb\u40db;\u4423blac;\u4170r;\uc000\ud835\udd18rave\u803b\xd9\u40d9acr;\u416a\u0100di\u1341\u1369er\u0100BP\u1348\u135d\u0100ar\u134d\u1350r;\u405fac\u0100ek\u1357\u1359;\u63dfet;\u63b5arenthesis;\u63ddon\u0100;P\u1370\u1371\u62c3lus;\u628e\u0100gp\u137b\u137fon;\u4172f;\uc000\ud835\udd4c\u0400ADETadps\u1395\u13ae\u13b8\u13c4\u03e8\u13d2\u13d7\u13f3rrow\u0180;BD\u1150\u13a0\u13a4ar;\u6912ownArrow;\u61c5ownArrow;\u6195quilibrium;\u696eee\u0100;A\u13cb\u13cc\u62a5rrow;\u61a5own\xe1\u03f3er\u0100LR\u13de\u13e8eftArrow;\u6196ightArrow;\u6197i\u0100;l\u13f9\u13fa\u43d2on;\u43a5ing;\u416ecr;\uc000\ud835\udcb0ilde;\u4168ml\u803b\xdc\u40dc\u0480Dbcdefosv\u1427\u142c\u1430\u1433\u143e\u1485\u148a\u1490\u1496ash;\u62abar;\u6aeby;\u4412ash\u0100;l\u143b\u143c\u62a9;\u6ae6\u0100er\u1443\u1445;\u62c1\u0180bty\u144c\u1450\u147aar;\u6016\u0100;i\u144f\u1455cal\u0200BLST\u1461\u1465\u146a\u1474ar;\u6223ine;\u407ceparator;\u6758ilde;\u6240ThinSpace;\u600ar;\uc000\ud835\udd19pf;\uc000\ud835\udd4dcr;\uc000\ud835\udcb1dash;\u62aa\u0280cefos\u14a7\u14ac\u14b1\u14b6\u14bcirc;\u4174dge;\u62c0r;\uc000\ud835\udd1apf;\uc000\ud835\udd4ecr;\uc000\ud835\udcb2\u0200fios\u14cb\u14d0\u14d2\u14d8r;\uc000\ud835\udd1b;\u439epf;\uc000\ud835\udd4fcr;\uc000\ud835\udcb3\u0480AIUacfosu\u14f1\u14f5\u14f9\u14fd\u1504\u150f\u1514\u151a\u1520cy;\u442fcy;\u4407cy;\u442ecute\u803b\xdd\u40dd\u0100iy\u1509\u150drc;\u4176;\u442br;\uc000\ud835\udd1cpf;\uc000\ud835\udd50cr;\uc000\ud835\udcb4ml;\u4178\u0400Hacdefos\u1535\u1539\u153f\u154b\u154f\u155d\u1560\u1564cy;\u4416cute;\u4179\u0100ay\u1544\u1549ron;\u417d;\u4417ot;\u417b\u01f2\u1554\0\u155boWidt\xe8\u0ad9a;\u4396r;\u6128pf;\u6124cr;\uc000\ud835\udcb5\u0be1\u1583\u158a\u1590\0\u15b0\u15b6\u15bf\0\0\0\0\u15c6\u15db\u15eb\u165f\u166d\0\u1695\u169b\u16b2\u16b9\0\u16becute\u803b\xe1\u40e1reve;\u4103\u0300;Ediuy\u159c\u159d\u15a1\u15a3\u15a8\u15ad\u623e;\uc000\u223e\u0333;\u623frc\u803b\xe2\u40e2te\u80bb\xb4\u0306;\u4430lig\u803b\xe6\u40e6\u0100;r\xb2\u15ba;\uc000\ud835\udd1erave\u803b\xe0\u40e0\u0100ep\u15ca\u15d6\u0100fp\u15cf\u15d4sym;\u6135\xe8\u15d3ha;\u43b1\u0100ap\u15dfc\u0100cl\u15e4\u15e7r;\u4101g;\u6a3f\u0264\u15f0\0\0\u160a\u0280;adsv\u15fa\u15fb\u15ff\u1601\u1607\u6227nd;\u6a55;\u6a5clope;\u6a58;\u6a5a\u0380;elmrsz\u1618\u1619\u161b\u161e\u163f\u164f\u1659\u6220;\u69a4e\xbb\u1619sd\u0100;a\u1625\u1626\u6221\u0461\u1630\u1632\u1634\u1636\u1638\u163a\u163c\u163e;\u69a8;\u69a9;\u69aa;\u69ab;\u69ac;\u69ad;\u69ae;\u69aft\u0100;v\u1645\u1646\u621fb\u0100;d\u164c\u164d\u62be;\u699d\u0100pt\u1654\u1657h;\u6222\xbb\xb9arr;\u637c\u0100gp\u1663\u1667on;\u4105f;\uc000\ud835\udd52\u0380;Eaeiop\u12c1\u167b\u167d\u1682\u1684\u1687\u168a;\u6a70cir;\u6a6f;\u624ad;\u624bs;\u4027rox\u0100;e\u12c1\u1692\xf1\u1683ing\u803b\xe5\u40e5\u0180cty\u16a1\u16a6\u16a8r;\uc000\ud835\udcb6;\u402amp\u0100;e\u12c1\u16af\xf1\u0288ilde\u803b\xe3\u40e3ml\u803b\xe4\u40e4\u0100ci\u16c2\u16c8onin\xf4\u0272nt;\u6a11\u0800Nabcdefiklnoprsu\u16ed\u16f1\u1730\u173c\u1743\u1748\u1778\u177d\u17e0\u17e6\u1839\u1850\u170d\u193d\u1948\u1970ot;\u6aed\u0100cr\u16f6\u171ek\u0200ceps\u1700\u1705\u170d\u1713ong;\u624cpsilon;\u43f6rime;\u6035im\u0100;e\u171a\u171b\u623dq;\u62cd\u0176\u1722\u1726ee;\u62bded\u0100;g\u172c\u172d\u6305e\xbb\u172drk\u0100;t\u135c\u1737brk;\u63b6\u0100oy\u1701\u1741;\u4431quo;\u601e\u0280cmprt\u1753\u175b\u1761\u1764\u1768aus\u0100;e\u010a\u0109ptyv;\u69b0s\xe9\u170cno\xf5\u0113\u0180ahw\u176f\u1771\u1773;\u43b2;\u6136een;\u626cr;\uc000\ud835\udd1fg\u0380costuvw\u178d\u179d\u17b3\u17c1\u17d5\u17db\u17de\u0180aiu\u1794\u1796\u179a\xf0\u0760rc;\u65efp\xbb\u1371\u0180dpt\u17a4\u17a8\u17adot;\u6a00lus;\u6a01imes;\u6a02\u0271\u17b9\0\0\u17becup;\u6a06ar;\u6605riangle\u0100du\u17cd\u17d2own;\u65bdp;\u65b3plus;\u6a04e\xe5\u1444\xe5\u14adarow;\u690d\u0180ako\u17ed\u1826\u1835\u0100cn\u17f2\u1823k\u0180lst\u17fa\u05ab\u1802ozenge;\u69ebriangle\u0200;dlr\u1812\u1813\u1818\u181d\u65b4own;\u65beeft;\u65c2ight;\u65b8k;\u6423\u01b1\u182b\0\u1833\u01b2\u182f\0\u1831;\u6592;\u65914;\u6593ck;\u6588\u0100eo\u183e\u184d\u0100;q\u1843\u1846\uc000=\u20e5uiv;\uc000\u2261\u20e5t;\u6310\u0200ptwx\u1859\u185e\u1867\u186cf;\uc000\ud835\udd53\u0100;t\u13cb\u1863om\xbb\u13cctie;\u62c8\u0600DHUVbdhmptuv\u1885\u1896\u18aa\u18bb\u18d7\u18db\u18ec\u18ff\u1905\u190a\u1910\u1921\u0200LRlr\u188e\u1890\u1892\u1894;\u6557;\u6554;\u6556;\u6553\u0280;DUdu\u18a1\u18a2\u18a4\u18a6\u18a8\u6550;\u6566;\u6569;\u6564;\u6567\u0200LRlr\u18b3\u18b5\u18b7\u18b9;\u655d;\u655a;\u655c;\u6559\u0380;HLRhlr\u18ca\u18cb\u18cd\u18cf\u18d1\u18d3\u18d5\u6551;\u656c;\u6563;\u6560;\u656b;\u6562;\u655fox;\u69c9\u0200LRlr\u18e4\u18e6\u18e8\u18ea;\u6555;\u6552;\u6510;\u650c\u0280;DUdu\u06bd\u18f7\u18f9\u18fb\u18fd;\u6565;\u6568;\u652c;\u6534inus;\u629flus;\u629eimes;\u62a0\u0200LRlr\u1919\u191b\u191d\u191f;\u655b;\u6558;\u6518;\u6514\u0380;HLRhlr\u1930\u1931\u1933\u1935\u1937\u1939\u193b\u6502;\u656a;\u6561;\u655e;\u653c;\u6524;\u651c\u0100ev\u0123\u1942bar\u803b\xa6\u40a6\u0200ceio\u1951\u1956\u195a\u1960r;\uc000\ud835\udcb7mi;\u604fm\u0100;e\u171a\u171cl\u0180;bh\u1968\u1969\u196b\u405c;\u69c5sub;\u67c8\u016c\u1974\u197el\u0100;e\u1979\u197a\u6022t\xbb\u197ap\u0180;Ee\u012f\u1985\u1987;\u6aae\u0100;q\u06dc\u06db\u0ce1\u19a7\0\u19e8\u1a11\u1a15\u1a32\0\u1a37\u1a50\0\0\u1ab4\0\0\u1ac1\0\0\u1b21\u1b2e\u1b4d\u1b52\0\u1bfd\0\u1c0c\u0180cpr\u19ad\u19b2\u19ddute;\u4107\u0300;abcds\u19bf\u19c0\u19c4\u19ca\u19d5\u19d9\u6229nd;\u6a44rcup;\u6a49\u0100au\u19cf\u19d2p;\u6a4bp;\u6a47ot;\u6a40;\uc000\u2229\ufe00\u0100eo\u19e2\u19e5t;\u6041\xee\u0693\u0200aeiu\u19f0\u19fb\u1a01\u1a05\u01f0\u19f5\0\u19f8s;\u6a4don;\u410ddil\u803b\xe7\u40e7rc;\u4109ps\u0100;s\u1a0c\u1a0d\u6a4cm;\u6a50ot;\u410b\u0180dmn\u1a1b\u1a20\u1a26il\u80bb\xb8\u01adptyv;\u69b2t\u8100\xa2;e\u1a2d\u1a2e\u40a2r\xe4\u01b2r;\uc000\ud835\udd20\u0180cei\u1a3d\u1a40\u1a4dy;\u4447ck\u0100;m\u1a47\u1a48\u6713ark\xbb\u1a48;\u43c7r\u0380;Ecefms\u1a5f\u1a60\u1a62\u1a6b\u1aa4\u1aaa\u1aae\u65cb;\u69c3\u0180;el\u1a69\u1a6a\u1a6d\u42c6q;\u6257e\u0261\u1a74\0\0\u1a88rrow\u0100lr\u1a7c\u1a81eft;\u61baight;\u61bb\u0280RSacd\u1a92\u1a94\u1a96\u1a9a\u1a9f\xbb\u0f47;\u64c8st;\u629birc;\u629aash;\u629dnint;\u6a10id;\u6aefcir;\u69c2ubs\u0100;u\u1abb\u1abc\u6663it\xbb\u1abc\u02ec\u1ac7\u1ad4\u1afa\0\u1b0aon\u0100;e\u1acd\u1ace\u403a\u0100;q\xc7\xc6\u026d\u1ad9\0\0\u1ae2a\u0100;t\u1ade\u1adf\u402c;\u4040\u0180;fl\u1ae8\u1ae9\u1aeb\u6201\xee\u1160e\u0100mx\u1af1\u1af6ent\xbb\u1ae9e\xf3\u024d\u01e7\u1afe\0\u1b07\u0100;d\u12bb\u1b02ot;\u6a6dn\xf4\u0246\u0180fry\u1b10\u1b14\u1b17;\uc000\ud835\udd54o\xe4\u0254\u8100\xa9;s\u0155\u1b1dr;\u6117\u0100ao\u1b25\u1b29rr;\u61b5ss;\u6717\u0100cu\u1b32\u1b37r;\uc000\ud835\udcb8\u0100bp\u1b3c\u1b44\u0100;e\u1b41\u1b42\u6acf;\u6ad1\u0100;e\u1b49\u1b4a\u6ad0;\u6ad2dot;\u62ef\u0380delprvw\u1b60\u1b6c\u1b77\u1b82\u1bac\u1bd4\u1bf9arr\u0100lr\u1b68\u1b6a;\u6938;\u6935\u0270\u1b72\0\0\u1b75r;\u62dec;\u62dfarr\u0100;p\u1b7f\u1b80\u61b6;\u693d\u0300;bcdos\u1b8f\u1b90\u1b96\u1ba1\u1ba5\u1ba8\u622arcap;\u6a48\u0100au\u1b9b\u1b9ep;\u6a46p;\u6a4aot;\u628dr;\u6a45;\uc000\u222a\ufe00\u0200alrv\u1bb5\u1bbf\u1bde\u1be3rr\u0100;m\u1bbc\u1bbd\u61b7;\u693cy\u0180evw\u1bc7\u1bd4\u1bd8q\u0270\u1bce\0\0\u1bd2re\xe3\u1b73u\xe3\u1b75ee;\u62ceedge;\u62cfen\u803b\xa4\u40a4earrow\u0100lr\u1bee\u1bf3eft\xbb\u1b80ight\xbb\u1bbde\xe4\u1bdd\u0100ci\u1c01\u1c07onin\xf4\u01f7nt;\u6231lcty;\u632d\u0980AHabcdefhijlorstuwz\u1c38\u1c3b\u1c3f\u1c5d\u1c69\u1c75\u1c8a\u1c9e\u1cac\u1cb7\u1cfb\u1cff\u1d0d\u1d7b\u1d91\u1dab\u1dbb\u1dc6\u1dcdr\xf2\u0381ar;\u6965\u0200glrs\u1c48\u1c4d\u1c52\u1c54ger;\u6020eth;\u6138\xf2\u1133h\u0100;v\u1c5a\u1c5b\u6010\xbb\u090a\u016b\u1c61\u1c67arow;\u690fa\xe3\u0315\u0100ay\u1c6e\u1c73ron;\u410f;\u4434\u0180;ao\u0332\u1c7c\u1c84\u0100gr\u02bf\u1c81r;\u61catseq;\u6a77\u0180glm\u1c91\u1c94\u1c98\u803b\xb0\u40b0ta;\u43b4ptyv;\u69b1\u0100ir\u1ca3\u1ca8sht;\u697f;\uc000\ud835\udd21ar\u0100lr\u1cb3\u1cb5\xbb\u08dc\xbb\u101e\u0280aegsv\u1cc2\u0378\u1cd6\u1cdc\u1ce0m\u0180;os\u0326\u1cca\u1cd4nd\u0100;s\u0326\u1cd1uit;\u6666amma;\u43ddin;\u62f2\u0180;io\u1ce7\u1ce8\u1cf8\u40f7de\u8100\xf7;o\u1ce7\u1cf0ntimes;\u62c7n\xf8\u1cf7cy;\u4452c\u026f\u1d06\0\0\u1d0arn;\u631eop;\u630d\u0280lptuw\u1d18\u1d1d\u1d22\u1d49\u1d55lar;\u4024f;\uc000\ud835\udd55\u0280;emps\u030b\u1d2d\u1d37\u1d3d\u1d42q\u0100;d\u0352\u1d33ot;\u6251inus;\u6238lus;\u6214quare;\u62a1blebarwedg\xe5\xfan\u0180adh\u112e\u1d5d\u1d67ownarrow\xf3\u1c83arpoon\u0100lr\u1d72\u1d76ef\xf4\u1cb4igh\xf4\u1cb6\u0162\u1d7f\u1d85karo\xf7\u0f42\u026f\u1d8a\0\0\u1d8ern;\u631fop;\u630c\u0180cot\u1d98\u1da3\u1da6\u0100ry\u1d9d\u1da1;\uc000\ud835\udcb9;\u4455l;\u69f6rok;\u4111\u0100dr\u1db0\u1db4ot;\u62f1i\u0100;f\u1dba\u1816\u65bf\u0100ah\u1dc0\u1dc3r\xf2\u0429a\xf2\u0fa6angle;\u69a6\u0100ci\u1dd2\u1dd5y;\u445fgrarr;\u67ff\u0900Dacdefglmnopqrstux\u1e01\u1e09\u1e19\u1e38\u0578\u1e3c\u1e49\u1e61\u1e7e\u1ea5\u1eaf\u1ebd\u1ee1\u1f2a\u1f37\u1f44\u1f4e\u1f5a\u0100Do\u1e06\u1d34o\xf4\u1c89\u0100cs\u1e0e\u1e14ute\u803b\xe9\u40e9ter;\u6a6e\u0200aioy\u1e22\u1e27\u1e31\u1e36ron;\u411br\u0100;c\u1e2d\u1e2e\u6256\u803b\xea\u40ealon;\u6255;\u444dot;\u4117\u0100Dr\u1e41\u1e45ot;\u6252;\uc000\ud835\udd22\u0180;rs\u1e50\u1e51\u1e57\u6a9aave\u803b\xe8\u40e8\u0100;d\u1e5c\u1e5d\u6a96ot;\u6a98\u0200;ils\u1e6a\u1e6b\u1e72\u1e74\u6a99nters;\u63e7;\u6113\u0100;d\u1e79\u1e7a\u6a95ot;\u6a97\u0180aps\u1e85\u1e89\u1e97cr;\u4113ty\u0180;sv\u1e92\u1e93\u1e95\u6205et\xbb\u1e93p\u01001;\u1e9d\u1ea4\u0133\u1ea1\u1ea3;\u6004;\u6005\u6003\u0100gs\u1eaa\u1eac;\u414bp;\u6002\u0100gp\u1eb4\u1eb8on;\u4119f;\uc000\ud835\udd56\u0180als\u1ec4\u1ece\u1ed2r\u0100;s\u1eca\u1ecb\u62d5l;\u69e3us;\u6a71i\u0180;lv\u1eda\u1edb\u1edf\u43b5on\xbb\u1edb;\u43f5\u0200csuv\u1eea\u1ef3\u1f0b\u1f23\u0100io\u1eef\u1e31rc\xbb\u1e2e\u0269\u1ef9\0\0\u1efb\xed\u0548ant\u0100gl\u1f02\u1f06tr\xbb\u1e5dess\xbb\u1e7a\u0180aei\u1f12\u1f16\u1f1als;\u403dst;\u625fv\u0100;D\u0235\u1f20D;\u6a78parsl;\u69e5\u0100Da\u1f2f\u1f33ot;\u6253rr;\u6971\u0180cdi\u1f3e\u1f41\u1ef8r;\u612fo\xf4\u0352\u0100ah\u1f49\u1f4b;\u43b7\u803b\xf0\u40f0\u0100mr\u1f53\u1f57l\u803b\xeb\u40ebo;\u60ac\u0180cip\u1f61\u1f64\u1f67l;\u4021s\xf4\u056e\u0100eo\u1f6c\u1f74ctatio\xee\u0559nential\xe5\u0579\u09e1\u1f92\0\u1f9e\0\u1fa1\u1fa7\0\0\u1fc6\u1fcc\0\u1fd3\0\u1fe6\u1fea\u2000\0\u2008\u205allingdotse\xf1\u1e44y;\u4444male;\u6640\u0180ilr\u1fad\u1fb3\u1fc1lig;\u8000\ufb03\u0269\u1fb9\0\0\u1fbdg;\u8000\ufb00ig;\u8000\ufb04;\uc000\ud835\udd23lig;\u8000\ufb01lig;\uc000fj\u0180alt\u1fd9\u1fdc\u1fe1t;\u666dig;\u8000\ufb02ns;\u65b1of;\u4192\u01f0\u1fee\0\u1ff3f;\uc000\ud835\udd57\u0100ak\u05bf\u1ff7\u0100;v\u1ffc\u1ffd\u62d4;\u6ad9artint;\u6a0d\u0100ao\u200c\u2055\u0100cs\u2011\u2052\u03b1\u201a\u2030\u2038\u2045\u2048\0\u2050\u03b2\u2022\u2025\u2027\u202a\u202c\0\u202e\u803b\xbd\u40bd;\u6153\u803b\xbc\u40bc;\u6155;\u6159;\u615b\u01b3\u2034\0\u2036;\u6154;\u6156\u02b4\u203e\u2041\0\0\u2043\u803b\xbe\u40be;\u6157;\u615c5;\u6158\u01b6\u204c\0\u204e;\u615a;\u615d8;\u615el;\u6044wn;\u6322cr;\uc000\ud835\udcbb\u0880Eabcdefgijlnorstv\u2082\u2089\u209f\u20a5\u20b0\u20b4\u20f0\u20f5\u20fa\u20ff\u2103\u2112\u2138\u0317\u213e\u2152\u219e\u0100;l\u064d\u2087;\u6a8c\u0180cmp\u2090\u2095\u209dute;\u41f5ma\u0100;d\u209c\u1cda\u43b3;\u6a86reve;\u411f\u0100iy\u20aa\u20aerc;\u411d;\u4433ot;\u4121\u0200;lqs\u063e\u0642\u20bd\u20c9\u0180;qs\u063e\u064c\u20c4lan\xf4\u0665\u0200;cdl\u0665\u20d2\u20d5\u20e5c;\u6aa9ot\u0100;o\u20dc\u20dd\u6a80\u0100;l\u20e2\u20e3\u6a82;\u6a84\u0100;e\u20ea\u20ed\uc000\u22db\ufe00s;\u6a94r;\uc000\ud835\udd24\u0100;g\u0673\u061bmel;\u6137cy;\u4453\u0200;Eaj\u065a\u210c\u210e\u2110;\u6a92;\u6aa5;\u6aa4\u0200Eaes\u211b\u211d\u2129\u2134;\u6269p\u0100;p\u2123\u2124\u6a8arox\xbb\u2124\u0100;q\u212e\u212f\u6a88\u0100;q\u212e\u211bim;\u62e7pf;\uc000\ud835\udd58\u0100ci\u2143\u2146r;\u610am\u0180;el\u066b\u214e\u2150;\u6a8e;\u6a90\u8300>;cdlqr\u05ee\u2160\u216a\u216e\u2173\u2179\u0100ci\u2165\u2167;\u6aa7r;\u6a7aot;\u62d7Par;\u6995uest;\u6a7c\u0280adels\u2184\u216a\u2190\u0656\u219b\u01f0\u2189\0\u218epro\xf8\u209er;\u6978q\u0100lq\u063f\u2196les\xf3\u2088i\xed\u066b\u0100en\u21a3\u21adrtneqq;\uc000\u2269\ufe00\xc5\u21aa\u0500Aabcefkosy\u21c4\u21c7\u21f1\u21f5\u21fa\u2218\u221d\u222f\u2268\u227dr\xf2\u03a0\u0200ilmr\u21d0\u21d4\u21d7\u21dbrs\xf0\u1484f\xbb\u2024il\xf4\u06a9\u0100dr\u21e0\u21e4cy;\u444a\u0180;cw\u08f4\u21eb\u21efir;\u6948;\u61adar;\u610firc;\u4125\u0180alr\u2201\u220e\u2213rts\u0100;u\u2209\u220a\u6665it\xbb\u220alip;\u6026con;\u62b9r;\uc000\ud835\udd25s\u0100ew\u2223\u2229arow;\u6925arow;\u6926\u0280amopr\u223a\u223e\u2243\u225e\u2263rr;\u61fftht;\u623bk\u0100lr\u2249\u2253eftarrow;\u61a9ightarrow;\u61aaf;\uc000\ud835\udd59bar;\u6015\u0180clt\u226f\u2274\u2278r;\uc000\ud835\udcbdas\xe8\u21f4rok;\u4127\u0100bp\u2282\u2287ull;\u6043hen\xbb\u1c5b\u0ae1\u22a3\0\u22aa\0\u22b8\u22c5\u22ce\0\u22d5\u22f3\0\0\u22f8\u2322\u2367\u2362\u237f\0\u2386\u23aa\u23b4cute\u803b\xed\u40ed\u0180;iy\u0771\u22b0\u22b5rc\u803b\xee\u40ee;\u4438\u0100cx\u22bc\u22bfy;\u4435cl\u803b\xa1\u40a1\u0100fr\u039f\u22c9;\uc000\ud835\udd26rave\u803b\xec\u40ec\u0200;ino\u073e\u22dd\u22e9\u22ee\u0100in\u22e2\u22e6nt;\u6a0ct;\u622dfin;\u69dcta;\u6129lig;\u4133\u0180aop\u22fe\u231a\u231d\u0180cgt\u2305\u2308\u2317r;\u412b\u0180elp\u071f\u230f\u2313in\xe5\u078ear\xf4\u0720h;\u4131f;\u62b7ed;\u41b5\u0280;cfot\u04f4\u232c\u2331\u233d\u2341are;\u6105in\u0100;t\u2338\u2339\u621eie;\u69dddo\xf4\u2319\u0280;celp\u0757\u234c\u2350\u235b\u2361al;\u62ba\u0100gr\u2355\u2359er\xf3\u1563\xe3\u234darhk;\u6a17rod;\u6a3c\u0200cgpt\u236f\u2372\u2376\u237by;\u4451on;\u412ff;\uc000\ud835\udd5aa;\u43b9uest\u803b\xbf\u40bf\u0100ci\u238a\u238fr;\uc000\ud835\udcben\u0280;Edsv\u04f4\u239b\u239d\u23a1\u04f3;\u62f9ot;\u62f5\u0100;v\u23a6\u23a7\u62f4;\u62f3\u0100;i\u0777\u23aelde;\u4129\u01eb\u23b8\0\u23bccy;\u4456l\u803b\xef\u40ef\u0300cfmosu\u23cc\u23d7\u23dc\u23e1\u23e7\u23f5\u0100iy\u23d1\u23d5rc;\u4135;\u4439r;\uc000\ud835\udd27ath;\u4237pf;\uc000\ud835\udd5b\u01e3\u23ec\0\u23f1r;\uc000\ud835\udcbfrcy;\u4458kcy;\u4454\u0400acfghjos\u240b\u2416\u2422\u2427\u242d\u2431\u2435\u243bppa\u0100;v\u2413\u2414\u43ba;\u43f0\u0100ey\u241b\u2420dil;\u4137;\u443ar;\uc000\ud835\udd28reen;\u4138cy;\u4445cy;\u445cpf;\uc000\ud835\udd5ccr;\uc000\ud835\udcc0\u0b80ABEHabcdefghjlmnoprstuv\u2470\u2481\u2486\u248d\u2491\u250e\u253d\u255a\u2580\u264e\u265e\u2665\u2679\u267d\u269a\u26b2\u26d8\u275d\u2768\u278b\u27c0\u2801\u2812\u0180art\u2477\u247a\u247cr\xf2\u09c6\xf2\u0395ail;\u691barr;\u690e\u0100;g\u0994\u248b;\u6a8bar;\u6962\u0963\u24a5\0\u24aa\0\u24b1\0\0\0\0\0\u24b5\u24ba\0\u24c6\u24c8\u24cd\0\u24f9ute;\u413amptyv;\u69b4ra\xee\u084cbda;\u43bbg\u0180;dl\u088e\u24c1\u24c3;\u6991\xe5\u088e;\u6a85uo\u803b\xab\u40abr\u0400;bfhlpst\u0899\u24de\u24e6\u24e9\u24eb\u24ee\u24f1\u24f5\u0100;f\u089d\u24e3s;\u691fs;\u691d\xeb\u2252p;\u61abl;\u6939im;\u6973l;\u61a2\u0180;ae\u24ff\u2500\u2504\u6aabil;\u6919\u0100;s\u2509\u250a\u6aad;\uc000\u2aad\ufe00\u0180abr\u2515\u2519\u251drr;\u690crk;\u6772\u0100ak\u2522\u252cc\u0100ek\u2528\u252a;\u407b;\u405b\u0100es\u2531\u2533;\u698bl\u0100du\u2539\u253b;\u698f;\u698d\u0200aeuy\u2546\u254b\u2556\u2558ron;\u413e\u0100di\u2550\u2554il;\u413c\xec\u08b0\xe2\u2529;\u443b\u0200cqrs\u2563\u2566\u256d\u257da;\u6936uo\u0100;r\u0e19\u1746\u0100du\u2572\u2577har;\u6967shar;\u694bh;\u61b2\u0280;fgqs\u258b\u258c\u0989\u25f3\u25ff\u6264t\u0280ahlrt\u2598\u25a4\u25b7\u25c2\u25e8rrow\u0100;t\u0899\u25a1a\xe9\u24f6arpoon\u0100du\u25af\u25b4own\xbb\u045ap\xbb\u0966eftarrows;\u61c7ight\u0180ahs\u25cd\u25d6\u25derrow\u0100;s\u08f4\u08a7arpoon\xf3\u0f98quigarro\xf7\u21f0hreetimes;\u62cb\u0180;qs\u258b\u0993\u25falan\xf4\u09ac\u0280;cdgs\u09ac\u260a\u260d\u261d\u2628c;\u6aa8ot\u0100;o\u2614\u2615\u6a7f\u0100;r\u261a\u261b\u6a81;\u6a83\u0100;e\u2622\u2625\uc000\u22da\ufe00s;\u6a93\u0280adegs\u2633\u2639\u263d\u2649\u264bppro\xf8\u24c6ot;\u62d6q\u0100gq\u2643\u2645\xf4\u0989gt\xf2\u248c\xf4\u099bi\xed\u09b2\u0180ilr\u2655\u08e1\u265asht;\u697c;\uc000\ud835\udd29\u0100;E\u099c\u2663;\u6a91\u0161\u2669\u2676r\u0100du\u25b2\u266e\u0100;l\u0965\u2673;\u696alk;\u6584cy;\u4459\u0280;acht\u0a48\u2688\u268b\u2691\u2696r\xf2\u25c1orne\xf2\u1d08ard;\u696bri;\u65fa\u0100io\u269f\u26a4dot;\u4140ust\u0100;a\u26ac\u26ad\u63b0che\xbb\u26ad\u0200Eaes\u26bb\u26bd\u26c9\u26d4;\u6268p\u0100;p\u26c3\u26c4\u6a89rox\xbb\u26c4\u0100;q\u26ce\u26cf\u6a87\u0100;q\u26ce\u26bbim;\u62e6\u0400abnoptwz\u26e9\u26f4\u26f7\u271a\u272f\u2741\u2747\u2750\u0100nr\u26ee\u26f1g;\u67ecr;\u61fdr\xeb\u08c1g\u0180lmr\u26ff\u270d\u2714eft\u0100ar\u09e6\u2707ight\xe1\u09f2apsto;\u67fcight\xe1\u09fdparrow\u0100lr\u2725\u2729ef\xf4\u24edight;\u61ac\u0180afl\u2736\u2739\u273dr;\u6985;\uc000\ud835\udd5dus;\u6a2dimes;\u6a34\u0161\u274b\u274fst;\u6217\xe1\u134e\u0180;ef\u2757\u2758\u1800\u65cange\xbb\u2758ar\u0100;l\u2764\u2765\u4028t;\u6993\u0280achmt\u2773\u2776\u277c\u2785\u2787r\xf2\u08a8orne\xf2\u1d8car\u0100;d\u0f98\u2783;\u696d;\u600eri;\u62bf\u0300achiqt\u2798\u279d\u0a40\u27a2\u27ae\u27bbquo;\u6039r;\uc000\ud835\udcc1m\u0180;eg\u09b2\u27aa\u27ac;\u6a8d;\u6a8f\u0100bu\u252a\u27b3o\u0100;r\u0e1f\u27b9;\u601arok;\u4142\u8400<;cdhilqr\u082b\u27d2\u2639\u27dc\u27e0\u27e5\u27ea\u27f0\u0100ci\u27d7\u27d9;\u6aa6r;\u6a79re\xe5\u25f2mes;\u62c9arr;\u6976uest;\u6a7b\u0100Pi\u27f5\u27f9ar;\u6996\u0180;ef\u2800\u092d\u181b\u65c3r\u0100du\u2807\u280dshar;\u694ahar;\u6966\u0100en\u2817\u2821rtneqq;\uc000\u2268\ufe00\xc5\u281e\u0700Dacdefhilnopsu\u2840\u2845\u2882\u288e\u2893\u28a0\u28a5\u28a8\u28da\u28e2\u28e4\u0a83\u28f3\u2902Dot;\u623a\u0200clpr\u284e\u2852\u2863\u287dr\u803b\xaf\u40af\u0100et\u2857\u2859;\u6642\u0100;e\u285e\u285f\u6720se\xbb\u285f\u0100;s\u103b\u2868to\u0200;dlu\u103b\u2873\u2877\u287bow\xee\u048cef\xf4\u090f\xf0\u13d1ker;\u65ae\u0100oy\u2887\u288cmma;\u6a29;\u443cash;\u6014asuredangle\xbb\u1626r;\uc000\ud835\udd2ao;\u6127\u0180cdn\u28af\u28b4\u28c9ro\u803b\xb5\u40b5\u0200;acd\u1464\u28bd\u28c0\u28c4s\xf4\u16a7ir;\u6af0ot\u80bb\xb7\u01b5us\u0180;bd\u28d2\u1903\u28d3\u6212\u0100;u\u1d3c\u28d8;\u6a2a\u0163\u28de\u28e1p;\u6adb\xf2\u2212\xf0\u0a81\u0100dp\u28e9\u28eeels;\u62a7f;\uc000\ud835\udd5e\u0100ct\u28f8\u28fdr;\uc000\ud835\udcc2pos\xbb\u159d\u0180;lm\u2909\u290a\u290d\u43bctimap;\u62b8\u0c00GLRVabcdefghijlmoprstuvw\u2942\u2953\u297e\u2989\u2998\u29da\u29e9\u2a15\u2a1a\u2a58\u2a5d\u2a83\u2a95\u2aa4\u2aa8\u2b04\u2b07\u2b44\u2b7f\u2bae\u2c34\u2c67\u2c7c\u2ce9\u0100gt\u2947\u294b;\uc000\u22d9\u0338\u0100;v\u2950\u0bcf\uc000\u226b\u20d2\u0180elt\u295a\u2972\u2976ft\u0100ar\u2961\u2967rrow;\u61cdightarrow;\u61ce;\uc000\u22d8\u0338\u0100;v\u297b\u0c47\uc000\u226a\u20d2ightarrow;\u61cf\u0100Dd\u298e\u2993ash;\u62afash;\u62ae\u0280bcnpt\u29a3\u29a7\u29ac\u29b1\u29ccla\xbb\u02deute;\u4144g;\uc000\u2220\u20d2\u0280;Eiop\u0d84\u29bc\u29c0\u29c5\u29c8;\uc000\u2a70\u0338d;\uc000\u224b\u0338s;\u4149ro\xf8\u0d84ur\u0100;a\u29d3\u29d4\u666el\u0100;s\u29d3\u0b38\u01f3\u29df\0\u29e3p\u80bb\xa0\u0b37mp\u0100;e\u0bf9\u0c00\u0280aeouy\u29f4\u29fe\u2a03\u2a10\u2a13\u01f0\u29f9\0\u29fb;\u6a43on;\u4148dil;\u4146ng\u0100;d\u0d7e\u2a0aot;\uc000\u2a6d\u0338p;\u6a42;\u443dash;\u6013\u0380;Aadqsx\u0b92\u2a29\u2a2d\u2a3b\u2a41\u2a45\u2a50rr;\u61d7r\u0100hr\u2a33\u2a36k;\u6924\u0100;o\u13f2\u13f0ot;\uc000\u2250\u0338ui\xf6\u0b63\u0100ei\u2a4a\u2a4ear;\u6928\xed\u0b98ist\u0100;s\u0ba0\u0b9fr;\uc000\ud835\udd2b\u0200Eest\u0bc5\u2a66\u2a79\u2a7c\u0180;qs\u0bbc\u2a6d\u0be1\u0180;qs\u0bbc\u0bc5\u2a74lan\xf4\u0be2i\xed\u0bea\u0100;r\u0bb6\u2a81\xbb\u0bb7\u0180Aap\u2a8a\u2a8d\u2a91r\xf2\u2971rr;\u61aear;\u6af2\u0180;sv\u0f8d\u2a9c\u0f8c\u0100;d\u2aa1\u2aa2\u62fc;\u62facy;\u445a\u0380AEadest\u2ab7\u2aba\u2abe\u2ac2\u2ac5\u2af6\u2af9r\xf2\u2966;\uc000\u2266\u0338rr;\u619ar;\u6025\u0200;fqs\u0c3b\u2ace\u2ae3\u2aeft\u0100ar\u2ad4\u2ad9rro\xf7\u2ac1ightarro\xf7\u2a90\u0180;qs\u0c3b\u2aba\u2aealan\xf4\u0c55\u0100;s\u0c55\u2af4\xbb\u0c36i\xed\u0c5d\u0100;r\u0c35\u2afei\u0100;e\u0c1a\u0c25i\xe4\u0d90\u0100pt\u2b0c\u2b11f;\uc000\ud835\udd5f\u8180\xac;in\u2b19\u2b1a\u2b36\u40acn\u0200;Edv\u0b89\u2b24\u2b28\u2b2e;\uc000\u22f9\u0338ot;\uc000\u22f5\u0338\u01e1\u0b89\u2b33\u2b35;\u62f7;\u62f6i\u0100;v\u0cb8\u2b3c\u01e1\u0cb8\u2b41\u2b43;\u62fe;\u62fd\u0180aor\u2b4b\u2b63\u2b69r\u0200;ast\u0b7b\u2b55\u2b5a\u2b5flle\xec\u0b7bl;\uc000\u2afd\u20e5;\uc000\u2202\u0338lint;\u6a14\u0180;ce\u0c92\u2b70\u2b73u\xe5\u0ca5\u0100;c\u0c98\u2b78\u0100;e\u0c92\u2b7d\xf1\u0c98\u0200Aait\u2b88\u2b8b\u2b9d\u2ba7r\xf2\u2988rr\u0180;cw\u2b94\u2b95\u2b99\u619b;\uc000\u2933\u0338;\uc000\u219d\u0338ghtarrow\xbb\u2b95ri\u0100;e\u0ccb\u0cd6\u0380chimpqu\u2bbd\u2bcd\u2bd9\u2b04\u0b78\u2be4\u2bef\u0200;cer\u0d32\u2bc6\u0d37\u2bc9u\xe5\u0d45;\uc000\ud835\udcc3ort\u026d\u2b05\0\0\u2bd6ar\xe1\u2b56m\u0100;e\u0d6e\u2bdf\u0100;q\u0d74\u0d73su\u0100bp\u2beb\u2bed\xe5\u0cf8\xe5\u0d0b\u0180bcp\u2bf6\u2c11\u2c19\u0200;Ees\u2bff\u2c00\u0d22\u2c04\u6284;\uc000\u2ac5\u0338et\u0100;e\u0d1b\u2c0bq\u0100;q\u0d23\u2c00c\u0100;e\u0d32\u2c17\xf1\u0d38\u0200;Ees\u2c22\u2c23\u0d5f\u2c27\u6285;\uc000\u2ac6\u0338et\u0100;e\u0d58\u2c2eq\u0100;q\u0d60\u2c23\u0200gilr\u2c3d\u2c3f\u2c45\u2c47\xec\u0bd7lde\u803b\xf1\u40f1\xe7\u0c43iangle\u0100lr\u2c52\u2c5ceft\u0100;e\u0c1a\u2c5a\xf1\u0c26ight\u0100;e\u0ccb\u2c65\xf1\u0cd7\u0100;m\u2c6c\u2c6d\u43bd\u0180;es\u2c74\u2c75\u2c79\u4023ro;\u6116p;\u6007\u0480DHadgilrs\u2c8f\u2c94\u2c99\u2c9e\u2ca3\u2cb0\u2cb6\u2cd3\u2ce3ash;\u62adarr;\u6904p;\uc000\u224d\u20d2ash;\u62ac\u0100et\u2ca8\u2cac;\uc000\u2265\u20d2;\uc000>\u20d2nfin;\u69de\u0180Aet\u2cbd\u2cc1\u2cc5rr;\u6902;\uc000\u2264\u20d2\u0100;r\u2cca\u2ccd\uc000<\u20d2ie;\uc000\u22b4\u20d2\u0100At\u2cd8\u2cdcrr;\u6903rie;\uc000\u22b5\u20d2im;\uc000\u223c\u20d2\u0180Aan\u2cf0\u2cf4\u2d02rr;\u61d6r\u0100hr\u2cfa\u2cfdk;\u6923\u0100;o\u13e7\u13e5ear;\u6927\u1253\u1a95\0\0\0\0\0\0\0\0\0\0\0\0\0\u2d2d\0\u2d38\u2d48\u2d60\u2d65\u2d72\u2d84\u1b07\0\0\u2d8d\u2dab\0\u2dc8\u2dce\0\u2ddc\u2e19\u2e2b\u2e3e\u2e43\u0100cs\u2d31\u1a97ute\u803b\xf3\u40f3\u0100iy\u2d3c\u2d45r\u0100;c\u1a9e\u2d42\u803b\xf4\u40f4;\u443e\u0280abios\u1aa0\u2d52\u2d57\u01c8\u2d5alac;\u4151v;\u6a38old;\u69bclig;\u4153\u0100cr\u2d69\u2d6dir;\u69bf;\uc000\ud835\udd2c\u036f\u2d79\0\0\u2d7c\0\u2d82n;\u42dbave\u803b\xf2\u40f2;\u69c1\u0100bm\u2d88\u0df4ar;\u69b5\u0200acit\u2d95\u2d98\u2da5\u2da8r\xf2\u1a80\u0100ir\u2d9d\u2da0r;\u69beoss;\u69bbn\xe5\u0e52;\u69c0\u0180aei\u2db1\u2db5\u2db9cr;\u414dga;\u43c9\u0180cdn\u2dc0\u2dc5\u01cdron;\u43bf;\u69b6pf;\uc000\ud835\udd60\u0180ael\u2dd4\u2dd7\u01d2r;\u69b7rp;\u69b9\u0380;adiosv\u2dea\u2deb\u2dee\u2e08\u2e0d\u2e10\u2e16\u6228r\xf2\u1a86\u0200;efm\u2df7\u2df8\u2e02\u2e05\u6a5dr\u0100;o\u2dfe\u2dff\u6134f\xbb\u2dff\u803b\xaa\u40aa\u803b\xba\u40bagof;\u62b6r;\u6a56lope;\u6a57;\u6a5b\u0180clo\u2e1f\u2e21\u2e27\xf2\u2e01ash\u803b\xf8\u40f8l;\u6298i\u016c\u2e2f\u2e34de\u803b\xf5\u40f5es\u0100;a\u01db\u2e3as;\u6a36ml\u803b\xf6\u40f6bar;\u633d\u0ae1\u2e5e\0\u2e7d\0\u2e80\u2e9d\0\u2ea2\u2eb9\0\0\u2ecb\u0e9c\0\u2f13\0\0\u2f2b\u2fbc\0\u2fc8r\u0200;ast\u0403\u2e67\u2e72\u0e85\u8100\xb6;l\u2e6d\u2e6e\u40b6le\xec\u0403\u0269\u2e78\0\0\u2e7bm;\u6af3;\u6afdy;\u443fr\u0280cimpt\u2e8b\u2e8f\u2e93\u1865\u2e97nt;\u4025od;\u402eil;\u6030enk;\u6031r;\uc000\ud835\udd2d\u0180imo\u2ea8\u2eb0\u2eb4\u0100;v\u2ead\u2eae\u43c6;\u43d5ma\xf4\u0a76ne;\u660e\u0180;tv\u2ebf\u2ec0\u2ec8\u43c0chfork\xbb\u1ffd;\u43d6\u0100au\u2ecf\u2edfn\u0100ck\u2ed5\u2eddk\u0100;h\u21f4\u2edb;\u610e\xf6\u21f4s\u0480;abcdemst\u2ef3\u2ef4\u1908\u2ef9\u2efd\u2f04\u2f06\u2f0a\u2f0e\u402bcir;\u6a23ir;\u6a22\u0100ou\u1d40\u2f02;\u6a25;\u6a72n\u80bb\xb1\u0e9dim;\u6a26wo;\u6a27\u0180ipu\u2f19\u2f20\u2f25ntint;\u6a15f;\uc000\ud835\udd61nd\u803b\xa3\u40a3\u0500;Eaceinosu\u0ec8\u2f3f\u2f41\u2f44\u2f47\u2f81\u2f89\u2f92\u2f7e\u2fb6;\u6ab3p;\u6ab7u\xe5\u0ed9\u0100;c\u0ece\u2f4c\u0300;acens\u0ec8\u2f59\u2f5f\u2f66\u2f68\u2f7eppro\xf8\u2f43urlye\xf1\u0ed9\xf1\u0ece\u0180aes\u2f6f\u2f76\u2f7approx;\u6ab9qq;\u6ab5im;\u62e8i\xed\u0edfme\u0100;s\u2f88\u0eae\u6032\u0180Eas\u2f78\u2f90\u2f7a\xf0\u2f75\u0180dfp\u0eec\u2f99\u2faf\u0180als\u2fa0\u2fa5\u2faalar;\u632eine;\u6312urf;\u6313\u0100;t\u0efb\u2fb4\xef\u0efbrel;\u62b0\u0100ci\u2fc0\u2fc5r;\uc000\ud835\udcc5;\u43c8ncsp;\u6008\u0300fiopsu\u2fda\u22e2\u2fdf\u2fe5\u2feb\u2ff1r;\uc000\ud835\udd2epf;\uc000\ud835\udd62rime;\u6057cr;\uc000\ud835\udcc6\u0180aeo\u2ff8\u3009\u3013t\u0100ei\u2ffe\u3005rnion\xf3\u06b0nt;\u6a16st\u0100;e\u3010\u3011\u403f\xf1\u1f19\xf4\u0f14\u0a80ABHabcdefhilmnoprstux\u3040\u3051\u3055\u3059\u30e0\u310e\u312b\u3147\u3162\u3172\u318e\u3206\u3215\u3224\u3229\u3258\u326e\u3272\u3290\u32b0\u32b7\u0180art\u3047\u304a\u304cr\xf2\u10b3\xf2\u03ddail;\u691car\xf2\u1c65ar;\u6964\u0380cdenqrt\u3068\u3075\u3078\u307f\u308f\u3094\u30cc\u0100eu\u306d\u3071;\uc000\u223d\u0331te;\u4155i\xe3\u116emptyv;\u69b3g\u0200;del\u0fd1\u3089\u308b\u308d;\u6992;\u69a5\xe5\u0fd1uo\u803b\xbb\u40bbr\u0580;abcfhlpstw\u0fdc\u30ac\u30af\u30b7\u30b9\u30bc\u30be\u30c0\u30c3\u30c7\u30cap;\u6975\u0100;f\u0fe0\u30b4s;\u6920;\u6933s;\u691e\xeb\u225d\xf0\u272el;\u6945im;\u6974l;\u61a3;\u619d\u0100ai\u30d1\u30d5il;\u691ao\u0100;n\u30db\u30dc\u6236al\xf3\u0f1e\u0180abr\u30e7\u30ea\u30eer\xf2\u17e5rk;\u6773\u0100ak\u30f3\u30fdc\u0100ek\u30f9\u30fb;\u407d;\u405d\u0100es\u3102\u3104;\u698cl\u0100du\u310a\u310c;\u698e;\u6990\u0200aeuy\u3117\u311c\u3127\u3129ron;\u4159\u0100di\u3121\u3125il;\u4157\xec\u0ff2\xe2\u30fa;\u4440\u0200clqs\u3134\u3137\u313d\u3144a;\u6937dhar;\u6969uo\u0100;r\u020e\u020dh;\u61b3\u0180acg\u314e\u315f\u0f44l\u0200;ips\u0f78\u3158\u315b\u109cn\xe5\u10bbar\xf4\u0fa9t;\u65ad\u0180ilr\u3169\u1023\u316esht;\u697d;\uc000\ud835\udd2f\u0100ao\u3177\u3186r\u0100du\u317d\u317f\xbb\u047b\u0100;l\u1091\u3184;\u696c\u0100;v\u318b\u318c\u43c1;\u43f1\u0180gns\u3195\u31f9\u31fcht\u0300ahlrst\u31a4\u31b0\u31c2\u31d8\u31e4\u31eerrow\u0100;t\u0fdc\u31ada\xe9\u30c8arpoon\u0100du\u31bb\u31bfow\xee\u317ep\xbb\u1092eft\u0100ah\u31ca\u31d0rrow\xf3\u0feaarpoon\xf3\u0551ightarrows;\u61c9quigarro\xf7\u30cbhreetimes;\u62ccg;\u42daingdotse\xf1\u1f32\u0180ahm\u320d\u3210\u3213r\xf2\u0feaa\xf2\u0551;\u600foust\u0100;a\u321e\u321f\u63b1che\xbb\u321fmid;\u6aee\u0200abpt\u3232\u323d\u3240\u3252\u0100nr\u3237\u323ag;\u67edr;\u61fer\xeb\u1003\u0180afl\u3247\u324a\u324er;\u6986;\uc000\ud835\udd63us;\u6a2eimes;\u6a35\u0100ap\u325d\u3267r\u0100;g\u3263\u3264\u4029t;\u6994olint;\u6a12ar\xf2\u31e3\u0200achq\u327b\u3280\u10bc\u3285quo;\u603ar;\uc000\ud835\udcc7\u0100bu\u30fb\u328ao\u0100;r\u0214\u0213\u0180hir\u3297\u329b\u32a0re\xe5\u31f8mes;\u62cai\u0200;efl\u32aa\u1059\u1821\u32ab\u65b9tri;\u69celuhar;\u6968;\u611e\u0d61\u32d5\u32db\u32df\u332c\u3338\u3371\0\u337a\u33a4\0\0\u33ec\u33f0\0\u3428\u3448\u345a\u34ad\u34b1\u34ca\u34f1\0\u3616\0\0\u3633cute;\u415bqu\xef\u27ba\u0500;Eaceinpsy\u11ed\u32f3\u32f5\u32ff\u3302\u330b\u330f\u331f\u3326\u3329;\u6ab4\u01f0\u32fa\0\u32fc;\u6ab8on;\u4161u\xe5\u11fe\u0100;d\u11f3\u3307il;\u415frc;\u415d\u0180Eas\u3316\u3318\u331b;\u6ab6p;\u6abaim;\u62e9olint;\u6a13i\xed\u1204;\u4441ot\u0180;be\u3334\u1d47\u3335\u62c5;\u6a66\u0380Aacmstx\u3346\u334a\u3357\u335b\u335e\u3363\u336drr;\u61d8r\u0100hr\u3350\u3352\xeb\u2228\u0100;o\u0a36\u0a34t\u803b\xa7\u40a7i;\u403bwar;\u6929m\u0100in\u3369\xf0nu\xf3\xf1t;\u6736r\u0100;o\u3376\u2055\uc000\ud835\udd30\u0200acoy\u3382\u3386\u3391\u33a0rp;\u666f\u0100hy\u338b\u338fcy;\u4449;\u4448rt\u026d\u3399\0\0\u339ci\xe4\u1464ara\xec\u2e6f\u803b\xad\u40ad\u0100gm\u33a8\u33b4ma\u0180;fv\u33b1\u33b2\u33b2\u43c3;\u43c2\u0400;deglnpr\u12ab\u33c5\u33c9\u33ce\u33d6\u33de\u33e1\u33e6ot;\u6a6a\u0100;q\u12b1\u12b0\u0100;E\u33d3\u33d4\u6a9e;\u6aa0\u0100;E\u33db\u33dc\u6a9d;\u6a9fe;\u6246lus;\u6a24arr;\u6972ar\xf2\u113d\u0200aeit\u33f8\u3408\u340f\u3417\u0100ls\u33fd\u3404lsetm\xe9\u336ahp;\u6a33parsl;\u69e4\u0100dl\u1463\u3414e;\u6323\u0100;e\u341c\u341d\u6aaa\u0100;s\u3422\u3423\u6aac;\uc000\u2aac\ufe00\u0180flp\u342e\u3433\u3442tcy;\u444c\u0100;b\u3438\u3439\u402f\u0100;a\u343e\u343f\u69c4r;\u633ff;\uc000\ud835\udd64a\u0100dr\u344d\u0402es\u0100;u\u3454\u3455\u6660it\xbb\u3455\u0180csu\u3460\u3479\u349f\u0100au\u3465\u346fp\u0100;s\u1188\u346b;\uc000\u2293\ufe00p\u0100;s\u11b4\u3475;\uc000\u2294\ufe00u\u0100bp\u347f\u348f\u0180;es\u1197\u119c\u3486et\u0100;e\u1197\u348d\xf1\u119d\u0180;es\u11a8\u11ad\u3496et\u0100;e\u11a8\u349d\xf1\u11ae\u0180;af\u117b\u34a6\u05b0r\u0165\u34ab\u05b1\xbb\u117car\xf2\u1148\u0200cemt\u34b9\u34be\u34c2\u34c5r;\uc000\ud835\udcc8tm\xee\xf1i\xec\u3415ar\xe6\u11be\u0100ar\u34ce\u34d5r\u0100;f\u34d4\u17bf\u6606\u0100an\u34da\u34edight\u0100ep\u34e3\u34eapsilo\xee\u1ee0h\xe9\u2eafs\xbb\u2852\u0280bcmnp\u34fb\u355e\u1209\u358b\u358e\u0480;Edemnprs\u350e\u350f\u3511\u3515\u351e\u3523\u352c\u3531\u3536\u6282;\u6ac5ot;\u6abd\u0100;d\u11da\u351aot;\u6ac3ult;\u6ac1\u0100Ee\u3528\u352a;\u6acb;\u628alus;\u6abfarr;\u6979\u0180eiu\u353d\u3552\u3555t\u0180;en\u350e\u3545\u354bq\u0100;q\u11da\u350feq\u0100;q\u352b\u3528m;\u6ac7\u0100bp\u355a\u355c;\u6ad5;\u6ad3c\u0300;acens\u11ed\u356c\u3572\u3579\u357b\u3326ppro\xf8\u32faurlye\xf1\u11fe\xf1\u11f3\u0180aes\u3582\u3588\u331bppro\xf8\u331aq\xf1\u3317g;\u666a\u0680123;Edehlmnps\u35a9\u35ac\u35af\u121c\u35b2\u35b4\u35c0\u35c9\u35d5\u35da\u35df\u35e8\u35ed\u803b\xb9\u40b9\u803b\xb2\u40b2\u803b\xb3\u40b3;\u6ac6\u0100os\u35b9\u35bct;\u6abeub;\u6ad8\u0100;d\u1222\u35c5ot;\u6ac4s\u0100ou\u35cf\u35d2l;\u67c9b;\u6ad7arr;\u697bult;\u6ac2\u0100Ee\u35e4\u35e6;\u6acc;\u628blus;\u6ac0\u0180eiu\u35f4\u3609\u360ct\u0180;en\u121c\u35fc\u3602q\u0100;q\u1222\u35b2eq\u0100;q\u35e7\u35e4m;\u6ac8\u0100bp\u3611\u3613;\u6ad4;\u6ad6\u0180Aan\u361c\u3620\u362drr;\u61d9r\u0100hr\u3626\u3628\xeb\u222e\u0100;o\u0a2b\u0a29war;\u692alig\u803b\xdf\u40df\u0be1\u3651\u365d\u3660\u12ce\u3673\u3679\0\u367e\u36c2\0\0\0\0\0\u36db\u3703\0\u3709\u376c\0\0\0\u3787\u0272\u3656\0\0\u365bget;\u6316;\u43c4r\xeb\u0e5f\u0180aey\u3666\u366b\u3670ron;\u4165dil;\u4163;\u4442lrec;\u6315r;\uc000\ud835\udd31\u0200eiko\u3686\u369d\u36b5\u36bc\u01f2\u368b\0\u3691e\u01004f\u1284\u1281a\u0180;sv\u3698\u3699\u369b\u43b8ym;\u43d1\u0100cn\u36a2\u36b2k\u0100as\u36a8\u36aeppro\xf8\u12c1im\xbb\u12acs\xf0\u129e\u0100as\u36ba\u36ae\xf0\u12c1rn\u803b\xfe\u40fe\u01ec\u031f\u36c6\u22e7es\u8180\xd7;bd\u36cf\u36d0\u36d8\u40d7\u0100;a\u190f\u36d5r;\u6a31;\u6a30\u0180eps\u36e1\u36e3\u3700\xe1\u2a4d\u0200;bcf\u0486\u36ec\u36f0\u36f4ot;\u6336ir;\u6af1\u0100;o\u36f9\u36fc\uc000\ud835\udd65rk;\u6ada\xe1\u3362rime;\u6034\u0180aip\u370f\u3712\u3764d\xe5\u1248\u0380adempst\u3721\u374d\u3740\u3751\u3757\u375c\u375fngle\u0280;dlqr\u3730\u3731\u3736\u3740\u3742\u65b5own\xbb\u1dbbeft\u0100;e\u2800\u373e\xf1\u092e;\u625cight\u0100;e\u32aa\u374b\xf1\u105aot;\u65ecinus;\u6a3alus;\u6a39b;\u69cdime;\u6a3bezium;\u63e2\u0180cht\u3772\u377d\u3781\u0100ry\u3777\u377b;\uc000\ud835\udcc9;\u4446cy;\u445brok;\u4167\u0100io\u378b\u378ex\xf4\u1777head\u0100lr\u3797\u37a0eftarro\xf7\u084fightarrow\xbb\u0f5d\u0900AHabcdfghlmoprstuw\u37d0\u37d3\u37d7\u37e4\u37f0\u37fc\u380e\u381c\u3823\u3834\u3851\u385d\u386b\u38a9\u38cc\u38d2\u38ea\u38f6r\xf2\u03edar;\u6963\u0100cr\u37dc\u37e2ute\u803b\xfa\u40fa\xf2\u1150r\u01e3\u37ea\0\u37edy;\u445eve;\u416d\u0100iy\u37f5\u37farc\u803b\xfb\u40fb;\u4443\u0180abh\u3803\u3806\u380br\xf2\u13adlac;\u4171a\xf2\u13c3\u0100ir\u3813\u3818sht;\u697e;\uc000\ud835\udd32rave\u803b\xf9\u40f9\u0161\u3827\u3831r\u0100lr\u382c\u382e\xbb\u0957\xbb\u1083lk;\u6580\u0100ct\u3839\u384d\u026f\u383f\0\0\u384arn\u0100;e\u3845\u3846\u631cr\xbb\u3846op;\u630fri;\u65f8\u0100al\u3856\u385acr;\u416b\u80bb\xa8\u0349\u0100gp\u3862\u3866on;\u4173f;\uc000\ud835\udd66\u0300adhlsu\u114b\u3878\u387d\u1372\u3891\u38a0own\xe1\u13b3arpoon\u0100lr\u3888\u388cef\xf4\u382digh\xf4\u382fi\u0180;hl\u3899\u389a\u389c\u43c5\xbb\u13faon\xbb\u389aparrows;\u61c8\u0180cit\u38b0\u38c4\u38c8\u026f\u38b6\0\0\u38c1rn\u0100;e\u38bc\u38bd\u631dr\xbb\u38bdop;\u630eng;\u416fri;\u65f9cr;\uc000\ud835\udcca\u0180dir\u38d9\u38dd\u38e2ot;\u62f0lde;\u4169i\u0100;f\u3730\u38e8\xbb\u1813\u0100am\u38ef\u38f2r\xf2\u38a8l\u803b\xfc\u40fcangle;\u69a7\u0780ABDacdeflnoprsz\u391c\u391f\u3929\u392d\u39b5\u39b8\u39bd\u39df\u39e4\u39e8\u39f3\u39f9\u39fd\u3a01\u3a20r\xf2\u03f7ar\u0100;v\u3926\u3927\u6ae8;\u6ae9as\xe8\u03e1\u0100nr\u3932\u3937grt;\u699c\u0380eknprst\u34e3\u3946\u394b\u3952\u395d\u3964\u3996app\xe1\u2415othin\xe7\u1e96\u0180hir\u34eb\u2ec8\u3959op\xf4\u2fb5\u0100;h\u13b7\u3962\xef\u318d\u0100iu\u3969\u396dgm\xe1\u33b3\u0100bp\u3972\u3984setneq\u0100;q\u397d\u3980\uc000\u228a\ufe00;\uc000\u2acb\ufe00setneq\u0100;q\u398f\u3992\uc000\u228b\ufe00;\uc000\u2acc\ufe00\u0100hr\u399b\u399fet\xe1\u369ciangle\u0100lr\u39aa\u39afeft\xbb\u0925ight\xbb\u1051y;\u4432ash\xbb\u1036\u0180elr\u39c4\u39d2\u39d7\u0180;be\u2dea\u39cb\u39cfar;\u62bbq;\u625alip;\u62ee\u0100bt\u39dc\u1468a\xf2\u1469r;\uc000\ud835\udd33tr\xe9\u39aesu\u0100bp\u39ef\u39f1\xbb\u0d1c\xbb\u0d59pf;\uc000\ud835\udd67ro\xf0\u0efbtr\xe9\u39b4\u0100cu\u3a06\u3a0br;\uc000\ud835\udccb\u0100bp\u3a10\u3a18n\u0100Ee\u3980\u3a16\xbb\u397en\u0100Ee\u3992\u3a1e\xbb\u3990igzag;\u699a\u0380cefoprs\u3a36\u3a3b\u3a56\u3a5b\u3a54\u3a61\u3a6airc;\u4175\u0100di\u3a40\u3a51\u0100bg\u3a45\u3a49ar;\u6a5fe\u0100;q\u15fa\u3a4f;\u6259erp;\u6118r;\uc000\ud835\udd34pf;\uc000\ud835\udd68\u0100;e\u1479\u3a66at\xe8\u1479cr;\uc000\ud835\udccc\u0ae3\u178e\u3a87\0\u3a8b\0\u3a90\u3a9b\0\0\u3a9d\u3aa8\u3aab\u3aaf\0\0\u3ac3\u3ace\0\u3ad8\u17dc\u17dftr\xe9\u17d1r;\uc000\ud835\udd35\u0100Aa\u3a94\u3a97r\xf2\u03c3r\xf2\u09f6;\u43be\u0100Aa\u3aa1\u3aa4r\xf2\u03b8r\xf2\u09eba\xf0\u2713is;\u62fb\u0180dpt\u17a4\u3ab5\u3abe\u0100fl\u3aba\u17a9;\uc000\ud835\udd69im\xe5\u17b2\u0100Aa\u3ac7\u3acar\xf2\u03cer\xf2\u0a01\u0100cq\u3ad2\u17b8r;\uc000\ud835\udccd\u0100pt\u17d6\u3adcr\xe9\u17d4\u0400acefiosu\u3af0\u3afd\u3b08\u3b0c\u3b11\u3b15\u3b1b\u3b21c\u0100uy\u3af6\u3afbte\u803b\xfd\u40fd;\u444f\u0100iy\u3b02\u3b06rc;\u4177;\u444bn\u803b\xa5\u40a5r;\uc000\ud835\udd36cy;\u4457pf;\uc000\ud835\udd6acr;\uc000\ud835\udcce\u0100cm\u3b26\u3b29y;\u444el\u803b\xff\u40ff\u0500acdefhiosw\u3b42\u3b48\u3b54\u3b58\u3b64\u3b69\u3b6d\u3b74\u3b7a\u3b80cute;\u417a\u0100ay\u3b4d\u3b52ron;\u417e;\u4437ot;\u417c\u0100et\u3b5d\u3b61tr\xe6\u155fa;\u43b6r;\uc000\ud835\udd37cy;\u4436grarr;\u61ddpf;\uc000\ud835\udd6bcr;\uc000\ud835\udccf\u0100jn\u3b85\u3b87;\u600dj;\u600c'.split("").map((t=>t.charCodeAt(0)))),w=new Uint16Array("\u0200aglq\t\x15\x18\x1b\u026d\x0f\0\0\x12p;\u4026os;\u4027t;\u403et;\u403cuot;\u4022".split("").map((t=>t.charCodeAt(0))));const v=new Map([[0,65533],[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),z=null!==(k=String.fromCodePoint)&&void 0!==k?k:function(t){let e="";return t>65535&&(t-=65536,e+=String.fromCharCode(t>>>10&1023|55296),t=56320|1023&t),e+=String.fromCharCode(t),e};var S;!function(t){t[t.NUM=35]="NUM",t[t.SEMI=59]="SEMI",t[t.EQUALS=61]="EQUALS",t[t.ZERO=48]="ZERO",t[t.NINE=57]="NINE",t[t.LOWER_A=97]="LOWER_A",t[t.LOWER_F=102]="LOWER_F",t[t.LOWER_X=120]="LOWER_X",t[t.LOWER_Z=122]="LOWER_Z",t[t.UPPER_A=65]="UPPER_A",t[t.UPPER_F=70]="UPPER_F",t[t.UPPER_Z=90]="UPPER_Z"}(S||(S={}));var q,B,L;function I(t){return t>=S.ZERO&&t<=S.NINE}function M(t){return t>=S.UPPER_A&&t<=S.UPPER_F||t>=S.LOWER_A&&t<=S.LOWER_F}function T(t){return t===S.EQUALS||function(t){return t>=S.UPPER_A&&t<=S.UPPER_Z||t>=S.LOWER_A&&t<=S.LOWER_Z||I(t)}(t)}!function(t){t[t.VALUE_LENGTH=49152]="VALUE_LENGTH",t[t.BRANCH_LENGTH=16256]="BRANCH_LENGTH",t[t.JUMP_TABLE=127]="JUMP_TABLE"}(q||(q={})),function(t){t[t.EntityStart=0]="EntityStart",t[t.NumericStart=1]="NumericStart",t[t.NumericDecimal=2]="NumericDecimal",t[t.NumericHex=3]="NumericHex",t[t.NamedEntity=4]="NamedEntity"}(B||(B={})),function(t){t[t.Legacy=0]="Legacy",t[t.Strict=1]="Strict",t[t.Attribute=2]="Attribute"}(L||(L={}));class R{constructor(t,e,r){this.decodeTree=t,this.emitCodePoint=e,this.errors=r,this.state=B.EntityStart,this.consumed=1,this.result=0,this.treeIndex=0,this.excess=1,this.decodeMode=L.Strict}startEntity(t){this.decodeMode=t,this.state=B.EntityStart,this.result=0,this.treeIndex=0,this.excess=1,this.consumed=1}write(t,e){switch(this.state){case B.EntityStart:return t.charCodeAt(e)===S.NUM?(this.state=B.NumericStart,this.consumed+=1,this.stateNumericStart(t,e+1)):(this.state=B.NamedEntity,this.stateNamedEntity(t,e));case B.NumericStart:return this.stateNumericStart(t,e);case B.NumericDecimal:return this.stateNumericDecimal(t,e);case B.NumericHex:return this.stateNumericHex(t,e);case B.NamedEntity:return this.stateNamedEntity(t,e)}}stateNumericStart(t,e){return e>=t.length?-1:(32|t.charCodeAt(e))===S.LOWER_X?(this.state=B.NumericHex,this.consumed+=1,this.stateNumericHex(t,e+1)):(this.state=B.NumericDecimal,this.stateNumericDecimal(t,e))}addToNumericResult(t,e,r,n){if(e!==r){const s=r-e;this.result=this.result*Math.pow(n,s)+parseInt(t.substr(e,s),n),this.consumed+=s}}stateNumericHex(t,e){const r=e;for(;e<t.length;){const n=t.charCodeAt(e);if(!I(n)&&!M(n))return this.addToNumericResult(t,r,e,16),this.emitNumericEntity(n,3);e+=1}return this.addToNumericResult(t,r,e,16),-1}stateNumericDecimal(t,e){const r=e;for(;e<t.length;){const n=t.charCodeAt(e);if(!I(n))return this.addToNumericResult(t,r,e,10),this.emitNumericEntity(n,2);e+=1}return this.addToNumericResult(t,r,e,10),-1}emitNumericEntity(t,e){var r;if(this.consumed<=e)return null===(r=this.errors)||void 0===r||r.absenceOfDigitsInNumericCharacterReference(this.consumed),0;if(t===S.SEMI)this.consumed+=1;else if(this.decodeMode===L.Strict)return 0;return this.emitCodePoint(function(t){var e;return t>=55296&&t<=57343||t>1114111?65533:null!==(e=v.get(t))&&void 0!==e?e:t}(this.result),this.consumed),this.errors&&(t!==S.SEMI&&this.errors.missingSemicolonAfterCharacterReference(),this.errors.validateNumericCharacterReference(this.result)),this.consumed}stateNamedEntity(t,e){const{decodeTree:r}=this;let n=r[this.treeIndex],s=(n&q.VALUE_LENGTH)>>14;for(;e<t.length;e++,this.excess++){const i=t.charCodeAt(e);if(this.treeIndex=P(r,n,this.treeIndex+Math.max(1,s),i),this.treeIndex<0)return 0===this.result||this.decodeMode===L.Attribute&&(0===s||T(i))?0:this.emitNotTerminatedNamedEntity();if(n=r[this.treeIndex],s=(n&q.VALUE_LENGTH)>>14,0!==s){if(i===S.SEMI)return this.emitNamedEntityData(this.treeIndex,s,this.consumed+this.excess);this.decodeMode!==L.Strict&&(this.result=this.treeIndex,this.consumed+=this.excess,this.excess=0)}}return-1}emitNotTerminatedNamedEntity(){var t;const{result:e,decodeTree:r}=this,n=(r[e]&q.VALUE_LENGTH)>>14;return this.emitNamedEntityData(e,n,this.consumed),null===(t=this.errors)||void 0===t||t.missingSemicolonAfterCharacterReference(),this.consumed}emitNamedEntityData(t,e,r){const{decodeTree:n}=this;return this.emitCodePoint(1===e?n[t]&~q.VALUE_LENGTH:n[t+1],r),3===e&&this.emitCodePoint(n[t+2],r),r}end(){var t;switch(this.state){case B.NamedEntity:return 0===this.result||this.decodeMode===L.Attribute&&this.result!==this.treeIndex?0:this.emitNotTerminatedNamedEntity();case B.NumericDecimal:return this.emitNumericEntity(0,2);case B.NumericHex:return this.emitNumericEntity(0,3);case B.NumericStart:return null===(t=this.errors)||void 0===t||t.absenceOfDigitsInNumericCharacterReference(this.consumed),0;case B.EntityStart:return 0}}}function N(t){let e="";const r=new R(t,(t=>e+=z(t)));return function(t,n){let s=0,i=0;for(;(i=t.indexOf("&",i))>=0;){e+=t.slice(s,i),r.startEntity(n);const o=r.write(t,i+1);if(o<0){s=i+r.end();break}s=i+o,i=0===o?s+1:s}const o=e+t.slice(s);return e="",o}}function P(t,e,r,n){const s=(e&q.BRANCH_LENGTH)>>7,i=e&q.JUMP_TABLE;if(0===s)return 0!==i&&n===i?r:-1;if(i){const e=n-i;return e<0||e>=s?-1:t[r+e]-1}let o=r,u=o+s-1;for(;o<=u;){const e=o+u>>>1,r=t[e];if(r<n)o=e+1;else{if(!(r>n))return t[e+s];u=e-1}}return-1}const O=N(x);function j(t,e=L.Legacy){return O(t,e)}function Z(t){return"[object String]"===function(t){return Object.prototype.toString.call(t)}(t)}N(w);const $=Object.prototype.hasOwnProperty;function U(t){return Array.prototype.slice.call(arguments,1).forEach((function(e){if(e){if("object"!=typeof e)throw new TypeError(e+"must be object");Object.keys(e).forEach((function(r){t[r]=e[r]}))}})),t}function H(t,e,r){return[].concat(t.slice(0,e),r,t.slice(e+1))}function V(t){return!(t>=55296&&t<=57343)&&(!(t>=64976&&t<=65007)&&(!!(65535&~t&&65534!=(65535&t))&&(!(t>=0&&t<=8)&&(11!==t&&(!(t>=14&&t<=31)&&(!(t>=127&&t<=159)&&!(t>1114111)))))))}function G(t){if(t>65535){const e=55296+((t-=65536)>>10),r=56320+(1023&t);return String.fromCharCode(e,r)}return String.fromCharCode(t)}const W=/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g,J=new RegExp(W.source+"|"+/&([a-z#][a-z0-9]{1,31});/gi.source,"gi"),Q=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))$/i;function X(t){return t.indexOf("\\")<0&&t.indexOf("&")<0?t:t.replace(J,(function(t,e,r){return e||function(t,e){if(35===e.charCodeAt(0)&&Q.test(e)){const r="x"===e[1].toLowerCase()?parseInt(e.slice(2),16):parseInt(e.slice(1),10);return V(r)?G(r):t}const r=j(t);return r!==t?r:t}(t,r)}))}const Y=/[&<>"]/,K=/[&<>"]/g,tt={"&":"&","<":"<",">":">",'"':"""};function et(t){return tt[t]}function rt(t){return Y.test(t)?t.replace(K,et):t}const nt=/[.?*+^$[\]\\(){}|-]/g;function st(t){switch(t){case 9:case 32:return!0}return!1}function it(t){if(t>=8192&&t<=8202)return!0;switch(t){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}function ot(t){return E.test(t)||A.test(t)}function ut(t){switch(t){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function ct(t){return t=t.trim().replace(/\s+/g," "),"\u1e7e"==="\u1e9e".toLowerCase()&&(t=t.replace(/\u1e9e/g,"\xdf")),t.toLowerCase().toUpperCase()}const at={mdurl:D,ucmicro:F};var lt=Object.freeze({__proto__:null,arrayReplaceAt:H,assign:U,escapeHtml:rt,escapeRE:function(t){return t.replace(nt,"\\$&")},fromCodePoint:G,has:function(t,e){return $.call(t,e)},isMdAsciiPunct:ut,isPunctChar:ot,isSpace:st,isString:Z,isValidEntityCode:V,isWhiteSpace:it,lib:at,normalizeReference:ct,unescapeAll:X,unescapeMd:function(t){return t.indexOf("\\")<0?t:t.replace(W,"$1")}});var ht=Object.freeze({__proto__:null,parseLinkDestination:function(t,e,r){let n,s=e;const i={ok:!1,pos:0,str:""};if(60===t.charCodeAt(s)){for(s++;s<r;){if(n=t.charCodeAt(s),10===n)return i;if(60===n)return i;if(62===n)return i.pos=s+1,i.str=X(t.slice(e+1,s)),i.ok=!0,i;92===n&&s+1<r?s+=2:s++}return i}let o=0;for(;s<r&&(n=t.charCodeAt(s),32!==n)&&!(n<32||127===n);)if(92===n&&s+1<r){if(32===t.charCodeAt(s+1))break;s+=2}else{if(40===n&&(o++,o>32))return i;if(41===n){if(0===o)break;o--}s++}return e===s||0!==o||(i.str=X(t.slice(e,s)),i.pos=s,i.ok=!0),i},parseLinkLabel:function(t,e,r){let n,s,i,o;const u=t.posMax,c=t.pos;for(t.pos=e+1,n=1;t.pos<u;){if(i=t.src.charCodeAt(t.pos),93===i&&(n--,0===n)){s=!0;break}if(o=t.pos,t.md.inline.skipToken(t),91===i)if(o===t.pos-1)n++;else if(r)return t.pos=c,-1}let a=-1;return s&&(a=t.pos),t.pos=c,a},parseLinkTitle:function(t,e,r,n){let s,i=e;const o={ok:!1,can_continue:!1,pos:0,str:"",marker:0};if(n)o.str=n.str,o.marker=n.marker;else{if(i>=r)return o;let n=t.charCodeAt(i);if(34!==n&&39!==n&&40!==n)return o;e++,i++,40===n&&(n=41),o.marker=n}for(;i<r;){if(s=t.charCodeAt(i),s===o.marker)return o.pos=i+1,o.str+=X(t.slice(e,i)),o.ok=!0,o;if(40===s&&41===o.marker)return o;92===s&&i+1<r&&i++,i++}return o.can_continue=!0,o.str+=X(t.slice(e,i)),o}});const pt={};function ft(){this.rules=U({},pt)}function dt(){this.__rules__=[],this.__cache__=null}function _t(t,e,r){this.type=t,this.tag=e,this.attrs=null,this.map=null,this.nesting=r,this.level=0,this.children=null,this.content="",this.markup="",this.info="",this.meta=null,this.block=!1,this.hidden=!1}function mt(t,e,r){this.src=t,this.env=r,this.tokens=[],this.inlineMode=!1,this.md=e}pt.code_inline=function(t,e,r,n,s){const i=t[e];return"<code"+s.renderAttrs(i)+">"+rt(i.content)+"</code>"},pt.code_block=function(t,e,r,n,s){const i=t[e];return"<pre"+s.renderAttrs(i)+"><code>"+rt(t[e].content)+"</code></pre>\n"},pt.fence=function(t,e,r,n,s){const i=t[e],o=i.info?X(i.info).trim():"";let u,c="",a="";if(o){const t=o.split(/(\s+)/g);c=t[0],a=t.slice(2).join("")}if(u=r.highlight&&r.highlight(i.content,c,a)||rt(i.content),0===u.indexOf("<pre"))return u+"\n";if(o){const t=i.attrIndex("class"),e=i.attrs?i.attrs.slice():[];t<0?e.push(["class",r.langPrefix+c]):(e[t]=e[t].slice(),e[t][1]+=" "+r.langPrefix+c);const n={attrs:e};return`<pre><code${s.renderAttrs(n)}>${u}</code></pre>\n`}return`<pre><code${s.renderAttrs(i)}>${u}</code></pre>\n`},pt.image=function(t,e,r,n,s){const i=t[e];return i.attrs[i.attrIndex("alt")][1]=s.renderInlineAsText(i.children,r,n),s.renderToken(t,e,r)},pt.hardbreak=function(t,e,r){return r.xhtmlOut?"<br />\n":"<br>\n"},pt.softbreak=function(t,e,r){return r.breaks?r.xhtmlOut?"<br />\n":"<br>\n":"\n"},pt.text=function(t,e){return rt(t[e].content)},pt.html_block=function(t,e){return t[e].content},pt.html_inline=function(t,e){return t[e].content},ft.prototype.renderAttrs=function(t){let e,r,n;if(!t.attrs)return"";for(n="",e=0,r=t.attrs.length;e<r;e++)n+=" "+rt(t.attrs[e][0])+'="'+rt(t.attrs[e][1])+'"';return n},ft.prototype.renderToken=function(t,e,r){const n=t[e];let s="";if(n.hidden)return"";n.block&&-1!==n.nesting&&e&&t[e-1].hidden&&(s+="\n"),s+=(-1===n.nesting?"</":"<")+n.tag,s+=this.renderAttrs(n),0===n.nesting&&r.xhtmlOut&&(s+=" /");let i=!1;if(n.block&&(i=!0,1===n.nesting&&e+1<t.length)){const r=t[e+1];("inline"===r.type||r.hidden||-1===r.nesting&&r.tag===n.tag)&&(i=!1)}return s+=i?">\n":">",s},ft.prototype.renderInline=function(t,e,r){let n="";const s=this.rules;for(let i=0,o=t.length;i<o;i++){const o=t[i].type;void 0!==s[o]?n+=s[o](t,i,e,r,this):n+=this.renderToken(t,i,e)}return n},ft.prototype.renderInlineAsText=function(t,e,r){let n="";for(let s=0,i=t.length;s<i;s++)switch(t[s].type){case"text":case"html_inline":case"html_block":n+=t[s].content;break;case"image":n+=this.renderInlineAsText(t[s].children,e,r);break;case"softbreak":case"hardbreak":n+="\n"}return n},ft.prototype.render=function(t,e,r){let n="";const s=this.rules;for(let i=0,o=t.length;i<o;i++){const o=t[i].type;"inline"===o?n+=this.renderInline(t[i].children,e,r):void 0!==s[o]?n+=s[o](t,i,e,r,this):n+=this.renderToken(t,i,e,r)}return n},dt.prototype.__find__=function(t){for(let e=0;e<this.__rules__.length;e++)if(this.__rules__[e].name===t)return e;return-1},dt.prototype.__compile__=function(){const t=this,e=[""];t.__rules__.forEach((function(t){t.enabled&&t.alt.forEach((function(t){e.indexOf(t)<0&&e.push(t)}))})),t.__cache__={},e.forEach((function(e){t.__cache__[e]=[],t.__rules__.forEach((function(r){r.enabled&&(e&&r.alt.indexOf(e)<0||t.__cache__[e].push(r.fn))}))}))},dt.prototype.at=function(t,e,r){const n=this.__find__(t),s=r||{};if(-1===n)throw new Error("Parser rule not found: "+t);this.__rules__[n].fn=e,this.__rules__[n].alt=s.alt||[],this.__cache__=null},dt.prototype.before=function(t,e,r,n){const s=this.__find__(t),i=n||{};if(-1===s)throw new Error("Parser rule not found: "+t);this.__rules__.splice(s,0,{name:e,enabled:!0,fn:r,alt:i.alt||[]}),this.__cache__=null},dt.prototype.after=function(t,e,r,n){const s=this.__find__(t),i=n||{};if(-1===s)throw new Error("Parser rule not found: "+t);this.__rules__.splice(s+1,0,{name:e,enabled:!0,fn:r,alt:i.alt||[]}),this.__cache__=null},dt.prototype.push=function(t,e,r){const n=r||{};this.__rules__.push({name:t,enabled:!0,fn:e,alt:n.alt||[]}),this.__cache__=null},dt.prototype.enable=function(t,e){Array.isArray(t)||(t=[t]);const r=[];return t.forEach((function(t){const n=this.__find__(t);if(n<0){if(e)return;throw new Error("Rules manager: invalid rule name "+t)}this.__rules__[n].enabled=!0,r.push(t)}),this),this.__cache__=null,r},dt.prototype.enableOnly=function(t,e){Array.isArray(t)||(t=[t]),this.__rules__.forEach((function(t){t.enabled=!1})),this.enable(t,e)},dt.prototype.disable=function(t,e){Array.isArray(t)||(t=[t]);const r=[];return t.forEach((function(t){const n=this.__find__(t);if(n<0){if(e)return;throw new Error("Rules manager: invalid rule name "+t)}this.__rules__[n].enabled=!1,r.push(t)}),this),this.__cache__=null,r},dt.prototype.getRules=function(t){return null===this.__cache__&&this.__compile__(),this.__cache__[t]||[]},_t.prototype.attrIndex=function(t){if(!this.attrs)return-1;const e=this.attrs;for(let r=0,n=e.length;r<n;r++)if(e[r][0]===t)return r;return-1},_t.prototype.attrPush=function(t){this.attrs?this.attrs.push(t):this.attrs=[t]},_t.prototype.attrSet=function(t,e){const r=this.attrIndex(t),n=[t,e];r<0?this.attrPush(n):this.attrs[r]=n},_t.prototype.attrGet=function(t){const e=this.attrIndex(t);let r=null;return e>=0&&(r=this.attrs[e][1]),r},_t.prototype.attrJoin=function(t,e){const r=this.attrIndex(t);r<0?this.attrPush([t,e]):this.attrs[r][1]=this.attrs[r][1]+" "+e},mt.prototype.Token=_t;const gt=/\r\n?|\n/g,kt=/\0/g;function Dt(t){return/^<\/a\s*>/i.test(t)}const Ct=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,yt=/\((c|tm|r)\)/i,Et=/\((c|tm|r)\)/gi,At={c:"\xa9",r:"\xae",tm:"\u2122"};function bt(t,e){return At[e.toLowerCase()]}function Ft(t){let e=0;for(let r=t.length-1;r>=0;r--){const n=t[r];"text"!==n.type||e||(n.content=n.content.replace(Et,bt)),"link_open"===n.type&&"auto"===n.info&&e--,"link_close"===n.type&&"auto"===n.info&&e++}}function xt(t){let e=0;for(let r=t.length-1;r>=0;r--){const n=t[r];"text"!==n.type||e||Ct.test(n.content)&&(n.content=n.content.replace(/\+-/g,"\xb1").replace(/\.{2,}/g,"\u2026").replace(/([?!])\u2026/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/gm,"$1\u2014").replace(/(^|\s)--(?=\s|$)/gm,"$1\u2013").replace(/(^|[^-\s])--(?=[^-\s]|$)/gm,"$1\u2013")),"link_open"===n.type&&"auto"===n.info&&e--,"link_close"===n.type&&"auto"===n.info&&e++}}const wt=/['"]/,vt=/['"]/g,zt="\u2019";function St(t,e,r){return t.slice(0,e)+r+t.slice(e+1)}function qt(t,e){let r;const n=[];for(let s=0;s<t.length;s++){const i=t[s],o=t[s].level;for(r=n.length-1;r>=0&&!(n[r].level<=o);r--);if(n.length=r+1,"text"!==i.type)continue;let u=i.content,c=0,a=u.length;t:for(;c<a;){vt.lastIndex=c;const l=vt.exec(u);if(!l)break;let h=!0,p=!0;c=l.index+1;const f="'"===l[0];let d=32;if(l.index-1>=0)d=u.charCodeAt(l.index-1);else for(r=s-1;r>=0&&("softbreak"!==t[r].type&&"hardbreak"!==t[r].type);r--)if(t[r].content){d=t[r].content.charCodeAt(t[r].content.length-1);break}let _=32;if(c<a)_=u.charCodeAt(c);else for(r=s+1;r<t.length&&("softbreak"!==t[r].type&&"hardbreak"!==t[r].type);r++)if(t[r].content){_=t[r].content.charCodeAt(0);break}const m=ut(d)||ot(String.fromCharCode(d)),g=ut(_)||ot(String.fromCharCode(_)),k=it(d),D=it(_);if(D?h=!1:g&&(k||m||(h=!1)),k?p=!1:m&&(D||g||(p=!1)),34===_&&'"'===l[0]&&d>=48&&d<=57&&(p=h=!1),h&&p&&(h=m,p=g),h||p){if(p)for(r=n.length-1;r>=0;r--){let h=n[r];if(n[r].level<o)break;if(h.single===f&&n[r].level===o){let o,p;h=n[r],f?(o=e.md.options.quotes[2],p=e.md.options.quotes[3]):(o=e.md.options.quotes[0],p=e.md.options.quotes[1]),i.content=St(i.content,l.index,p),t[h.token].content=St(t[h.token].content,h.pos,o),c+=p.length-1,h.token===s&&(c+=o.length-1),u=i.content,a=u.length,n.length=r;continue t}}h?n.push({token:s,pos:l.index,single:f,level:o}):p&&f&&(i.content=St(i.content,l.index,zt))}else f&&(i.content=St(i.content,l.index,zt))}}}const Bt=[["normalize",function(t){let e;e=t.src.replace(gt,"\n"),e=e.replace(kt,"\ufffd"),t.src=e}],["block",function(t){let e;t.inlineMode?(e=new t.Token("inline","",0),e.content=t.src,e.map=[0,1],e.children=[],t.tokens.push(e)):t.md.block.parse(t.src,t.md,t.env,t.tokens)}],["inline",function(t){const e=t.tokens;for(let r=0,n=e.length;r<n;r++){const n=e[r];"inline"===n.type&&t.md.inline.parse(n.content,t.md,t.env,n.children)}}],["linkify",function(t){const e=t.tokens;var r;if(t.md.options.linkify)for(let n=0,s=e.length;n<s;n++){if("inline"!==e[n].type||!t.md.linkify.pretest(e[n].content))continue;let s=e[n].children,i=0;for(let o=s.length-1;o>=0;o--){const u=s[o];if("link_close"!==u.type){if("html_inline"===u.type&&(r=u.content,/^<a[>\s]/i.test(r)&&i>0&&i--,Dt(u.content)&&i++),!(i>0)&&"text"===u.type&&t.md.linkify.test(u.content)){const r=u.content;let i=t.md.linkify.match(r);const c=[];let a=u.level,l=0;i.length>0&&0===i[0].index&&o>0&&"text_special"===s[o-1].type&&(i=i.slice(1));for(let e=0;e<i.length;e++){const n=i[e].url,s=t.md.normalizeLink(n);if(!t.md.validateLink(s))continue;let o=i[e].text;o=i[e].schema?"mailto:"!==i[e].schema||/^mailto:/i.test(o)?t.md.normalizeLinkText(o):t.md.normalizeLinkText("mailto:"+o).replace(/^mailto:/,""):t.md.normalizeLinkText("http://"+o).replace(/^http:\/\//,"");const u=i[e].index;if(u>l){const e=new t.Token("text","",0);e.content=r.slice(l,u),e.level=a,c.push(e)}const h=new t.Token("link_open","a",1);h.attrs=[["href",s]],h.level=a++,h.markup="linkify",h.info="auto",c.push(h);const p=new t.Token("text","",0);p.content=o,p.level=a,c.push(p);const f=new t.Token("link_close","a",-1);f.level=--a,f.markup="linkify",f.info="auto",c.push(f),l=i[e].lastIndex}if(l<r.length){const e=new t.Token("text","",0);e.content=r.slice(l),e.level=a,c.push(e)}e[n].children=s=H(s,o,c)}}else for(o--;s[o].level!==u.level&&"link_open"!==s[o].type;)o--}}}],["replacements",function(t){let e;if(t.md.options.typographer)for(e=t.tokens.length-1;e>=0;e--)"inline"===t.tokens[e].type&&(yt.test(t.tokens[e].content)&&Ft(t.tokens[e].children),Ct.test(t.tokens[e].content)&&xt(t.tokens[e].children))}],["smartquotes",function(t){if(t.md.options.typographer)for(let e=t.tokens.length-1;e>=0;e--)"inline"===t.tokens[e].type&&wt.test(t.tokens[e].content)&&qt(t.tokens[e].children,t)}],["text_join",function(t){let e,r;const n=t.tokens,s=n.length;for(let t=0;t<s;t++){if("inline"!==n[t].type)continue;const s=n[t].children,i=s.length;for(e=0;e<i;e++)"text_special"===s[e].type&&(s[e].type="text");for(e=r=0;e<i;e++)"text"===s[e].type&&e+1<i&&"text"===s[e+1].type?s[e+1].content=s[e].content+s[e+1].content:(e!==r&&(s[r]=s[e]),r++);e!==r&&(s.length=r)}}]];function Lt(){this.ruler=new dt;for(let t=0;t<Bt.length;t++)this.ruler.push(Bt[t][0],Bt[t][1])}function It(t,e,r,n){this.src=t,this.md=e,this.env=r,this.tokens=n,this.bMarks=[],this.eMarks=[],this.tShift=[],this.sCount=[],this.bsCount=[],this.blkIndent=0,this.line=0,this.lineMax=0,this.tight=!1,this.ddIndent=-1,this.listIndent=-1,this.parentType="root",this.level=0;const s=this.src;for(let t=0,e=0,r=0,n=0,i=s.length,o=!1;e<i;e++){const u=s.charCodeAt(e);if(!o){if(st(u)){r++,9===u?n+=4-n%4:n++;continue}o=!0}10!==u&&e!==i-1||(10!==u&&e++,this.bMarks.push(t),this.eMarks.push(e),this.tShift.push(r),this.sCount.push(n),this.bsCount.push(0),o=!1,r=0,n=0,t=e+1)}this.bMarks.push(s.length),this.eMarks.push(s.length),this.tShift.push(0),this.sCount.push(0),this.bsCount.push(0),this.lineMax=this.bMarks.length-1}Lt.prototype.process=function(t){const e=this.ruler.getRules("");for(let r=0,n=e.length;r<n;r++)e[r](t)},Lt.prototype.State=mt,It.prototype.push=function(t,e,r){const n=new _t(t,e,r);return n.block=!0,r<0&&this.level--,n.level=this.level,r>0&&this.level++,this.tokens.push(n),n},It.prototype.isEmpty=function(t){return this.bMarks[t]+this.tShift[t]>=this.eMarks[t]},It.prototype.skipEmptyLines=function(t){for(let e=this.lineMax;t<e&&!(this.bMarks[t]+this.tShift[t]<this.eMarks[t]);t++);return t},It.prototype.skipSpaces=function(t){for(let e=this.src.length;t<e;t++){if(!st(this.src.charCodeAt(t)))break}return t},It.prototype.skipSpacesBack=function(t,e){if(t<=e)return t;for(;t>e;)if(!st(this.src.charCodeAt(--t)))return t+1;return t},It.prototype.skipChars=function(t,e){for(let r=this.src.length;t<r&&this.src.charCodeAt(t)===e;t++);return t},It.prototype.skipCharsBack=function(t,e,r){if(t<=r)return t;for(;t>r;)if(e!==this.src.charCodeAt(--t))return t+1;return t},It.prototype.getLines=function(t,e,r,n){if(t>=e)return"";const s=new Array(e-t);for(let i=0,o=t;o<e;o++,i++){let t=0;const u=this.bMarks[o];let c,a=u;for(c=o+1<e||n?this.eMarks[o]+1:this.eMarks[o];a<c&&t<r;){const e=this.src.charCodeAt(a);if(st(e))9===e?t+=4-(t+this.bsCount[o])%4:t++;else{if(!(a-u<this.tShift[o]))break;t++}a++}s[i]=t>r?new Array(t-r+1).join(" ")+this.src.slice(a,c):this.src.slice(a,c)}return s.join("")},It.prototype.Token=_t;function Mt(t,e){const r=t.bMarks[e]+t.tShift[e],n=t.eMarks[e];return t.src.slice(r,n)}function Tt(t){const e=[],r=t.length;let n=0,s=t.charCodeAt(n),i=!1,o=0,u="";for(;n<r;)124===s&&(i?(u+=t.substring(o,n-1),o=n):(e.push(u+t.substring(o,n)),u="",o=n+1)),i=92===s,n++,s=t.charCodeAt(n);return e.push(u+t.substring(o)),e}function Rt(t,e){const r=t.eMarks[e];let n=t.bMarks[e]+t.tShift[e];const s=t.src.charCodeAt(n++);if(42!==s&&45!==s&&43!==s)return-1;if(n<r){if(!st(t.src.charCodeAt(n)))return-1}return n}function Nt(t,e){const r=t.bMarks[e]+t.tShift[e],n=t.eMarks[e];let s=r;if(s+1>=n)return-1;let i=t.src.charCodeAt(s++);if(i<48||i>57)return-1;for(;;){if(s>=n)return-1;if(i=t.src.charCodeAt(s++),!(i>=48&&i<=57)){if(41===i||46===i)break;return-1}if(s-r>=10)return-1}return s<n&&(i=t.src.charCodeAt(s),!st(i))?-1:s}const Pt="<[A-Za-z][A-Za-z0-9\\-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*\\/?>",Ot="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",jt=new RegExp("^(?:"+Pt+"|"+Ot+"|\x3c!---?>|\x3c!--(?:[^-]|-[^-]|--[^>])*--\x3e|<[?][\\s\\S]*?[?]>|<![A-Za-z][^>]*>|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>)"),Zt=new RegExp("^(?:"+Pt+"|"+Ot+")"),$t=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^<!--/,/-->/,!0],[/^<\?/,/\?>/,!0],[/^<![A-Z]/,/>/,!0],[/^<!\[CDATA\[/,/\]\]>/,!0],[new RegExp("^</?("+["address","article","aside","base","basefont","blockquote","body","caption","center","col","colgroup","dd","details","dialog","dir","div","dl","dt","fieldset","figcaption","figure","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hr","html","iframe","legend","li","link","main","menu","menuitem","nav","noframes","ol","optgroup","option","p","param","search","section","summary","table","tbody","td","tfoot","th","thead","title","tr","track","ul"].join("|")+")(?=(\\s|/?>|$))","i"),/^$/,!0],[new RegExp(Zt.source+"\\s*$"),/^$/,!1]];const Ut=[["table",function(t,e,r,n){if(e+2>r)return!1;let s=e+1;if(t.sCount[s]<t.blkIndent)return!1;if(t.sCount[s]-t.blkIndent>=4)return!1;let i=t.bMarks[s]+t.tShift[s];if(i>=t.eMarks[s])return!1;const o=t.src.charCodeAt(i++);if(124!==o&&45!==o&&58!==o)return!1;if(i>=t.eMarks[s])return!1;const u=t.src.charCodeAt(i++);if(124!==u&&45!==u&&58!==u&&!st(u))return!1;if(45===o&&st(u))return!1;for(;i<t.eMarks[s];){const e=t.src.charCodeAt(i);if(124!==e&&45!==e&&58!==e&&!st(e))return!1;i++}let c=Mt(t,e+1),a=c.split("|");const l=[];for(let t=0;t<a.length;t++){const e=a[t].trim();if(!e){if(0===t||t===a.length-1)continue;return!1}if(!/^:?-+:?$/.test(e))return!1;58===e.charCodeAt(e.length-1)?l.push(58===e.charCodeAt(0)?"center":"right"):58===e.charCodeAt(0)?l.push("left"):l.push("")}if(c=Mt(t,e).trim(),-1===c.indexOf("|"))return!1;if(t.sCount[e]-t.blkIndent>=4)return!1;a=Tt(c),a.length&&""===a[0]&&a.shift(),a.length&&""===a[a.length-1]&&a.pop();const h=a.length;if(0===h||h!==l.length)return!1;if(n)return!0;const p=t.parentType;t.parentType="table";const f=t.md.block.ruler.getRules("blockquote"),d=[e,0];t.push("table_open","table",1).map=d,t.push("thead_open","thead",1).map=[e,e+1],t.push("tr_open","tr",1).map=[e,e+1];for(let e=0;e<a.length;e++){const r=t.push("th_open","th",1);l[e]&&(r.attrs=[["style","text-align:"+l[e]]]);const n=t.push("inline","",0);n.content=a[e].trim(),n.children=[],t.push("th_close","th",-1)}let _;t.push("tr_close","tr",-1),t.push("thead_close","thead",-1);let m=0;for(s=e+2;s<r&&!(t.sCount[s]<t.blkIndent);s++){let n=!1;for(let e=0,i=f.length;e<i;e++)if(f[e](t,s,r,!0)){n=!0;break}if(n)break;if(c=Mt(t,s).trim(),!c)break;if(t.sCount[s]-t.blkIndent>=4)break;if(a=Tt(c),a.length&&""===a[0]&&a.shift(),a.length&&""===a[a.length-1]&&a.pop(),m+=h-a.length,m>65536)break;if(s===e+2){t.push("tbody_open","tbody",1).map=_=[e+2,0]}t.push("tr_open","tr",1).map=[s,s+1];for(let e=0;e<h;e++){const r=t.push("td_open","td",1);l[e]&&(r.attrs=[["style","text-align:"+l[e]]]);const n=t.push("inline","",0);n.content=a[e]?a[e].trim():"",n.children=[],t.push("td_close","td",-1)}t.push("tr_close","tr",-1)}return _&&(t.push("tbody_close","tbody",-1),_[1]=s),t.push("table_close","table",-1),d[1]=s,t.parentType=p,t.line=s,!0},["paragraph","reference"]],["code",function(t,e,r){if(t.sCount[e]-t.blkIndent<4)return!1;let n=e+1,s=n;for(;n<r;)if(t.isEmpty(n))n++;else{if(!(t.sCount[n]-t.blkIndent>=4))break;n++,s=n}t.line=s;const i=t.push("code_block","code",0);return i.content=t.getLines(e,s,4+t.blkIndent,!1)+"\n",i.map=[e,t.line],!0}],["fence",function(t,e,r,n){let s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e];if(t.sCount[e]-t.blkIndent>=4)return!1;if(s+3>i)return!1;const o=t.src.charCodeAt(s);if(126!==o&&96!==o)return!1;let u=s;s=t.skipChars(s,o);let c=s-u;if(c<3)return!1;const a=t.src.slice(u,s),l=t.src.slice(s,i);if(96===o&&l.indexOf(String.fromCharCode(o))>=0)return!1;if(n)return!0;let h=e,p=!1;for(;(h++,!(h>=r))&&(s=u=t.bMarks[h]+t.tShift[h],i=t.eMarks[h],!(s<i&&t.sCount[h]<t.blkIndent));)if(t.src.charCodeAt(s)===o&&!(t.sCount[h]-t.blkIndent>=4||(s=t.skipChars(s,o),s-u<c||(s=t.skipSpaces(s),s<i)))){p=!0;break}c=t.sCount[e],t.line=h+(p?1:0);const f=t.push("fence","code",0);return f.info=l,f.content=t.getLines(e+1,h,c,!0),f.markup=a,f.map=[e,t.line],!0},["paragraph","reference","blockquote","list"]],["blockquote",function(t,e,r,n){let s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e];const o=t.lineMax;if(t.sCount[e]-t.blkIndent>=4)return!1;if(62!==t.src.charCodeAt(s))return!1;if(n)return!0;const u=[],c=[],a=[],l=[],h=t.md.block.ruler.getRules("blockquote"),p=t.parentType;t.parentType="blockquote";let f,d=!1;for(f=e;f<r;f++){const e=t.sCount[f]<t.blkIndent;if(s=t.bMarks[f]+t.tShift[f],i=t.eMarks[f],s>=i)break;if(62===t.src.charCodeAt(s++)&&!e){let e,r,n=t.sCount[f]+1;32===t.src.charCodeAt(s)?(s++,n++,r=!1,e=!0):9===t.src.charCodeAt(s)?(e=!0,(t.bsCount[f]+n)%4==3?(s++,n++,r=!1):r=!0):e=!1;let o=n;for(u.push(t.bMarks[f]),t.bMarks[f]=s;s<i;){const e=t.src.charCodeAt(s);if(!st(e))break;9===e?o+=4-(o+t.bsCount[f]+(r?1:0))%4:o++,s++}d=s>=i,c.push(t.bsCount[f]),t.bsCount[f]=t.sCount[f]+1+(e?1:0),a.push(t.sCount[f]),t.sCount[f]=o-n,l.push(t.tShift[f]),t.tShift[f]=s-t.bMarks[f];continue}if(d)break;let n=!1;for(let e=0,s=h.length;e<s;e++)if(h[e](t,f,r,!0)){n=!0;break}if(n){t.lineMax=f,0!==t.blkIndent&&(u.push(t.bMarks[f]),c.push(t.bsCount[f]),l.push(t.tShift[f]),a.push(t.sCount[f]),t.sCount[f]-=t.blkIndent);break}u.push(t.bMarks[f]),c.push(t.bsCount[f]),l.push(t.tShift[f]),a.push(t.sCount[f]),t.sCount[f]=-1}const _=t.blkIndent;t.blkIndent=0;const m=t.push("blockquote_open","blockquote",1);m.markup=">";const g=[e,0];m.map=g,t.md.block.tokenize(t,e,f),t.push("blockquote_close","blockquote",-1).markup=">",t.lineMax=o,t.parentType=p,g[1]=t.line;for(let r=0;r<l.length;r++)t.bMarks[r+e]=u[r],t.tShift[r+e]=l[r],t.sCount[r+e]=a[r],t.bsCount[r+e]=c[r];return t.blkIndent=_,!0},["paragraph","reference","blockquote","list"]],["hr",function(t,e,r,n){const s=t.eMarks[e];if(t.sCount[e]-t.blkIndent>=4)return!1;let i=t.bMarks[e]+t.tShift[e];const o=t.src.charCodeAt(i++);if(42!==o&&45!==o&&95!==o)return!1;let u=1;for(;i<s;){const e=t.src.charCodeAt(i++);if(e!==o&&!st(e))return!1;e===o&&u++}if(u<3)return!1;if(n)return!0;t.line=e+1;const c=t.push("hr","hr",0);return c.map=[e,t.line],c.markup=Array(u+1).join(String.fromCharCode(o)),!0},["paragraph","reference","blockquote","list"]],["list",function(t,e,r,n){let s,i,o,u,c=e,a=!0;if(t.sCount[c]-t.blkIndent>=4)return!1;if(t.listIndent>=0&&t.sCount[c]-t.listIndent>=4&&t.sCount[c]<t.blkIndent)return!1;let l,h,p,f=!1;if(n&&"paragraph"===t.parentType&&t.sCount[c]>=t.blkIndent&&(f=!0),(p=Nt(t,c))>=0){if(l=!0,o=t.bMarks[c]+t.tShift[c],h=Number(t.src.slice(o,p-1)),f&&1!==h)return!1}else{if(!((p=Rt(t,c))>=0))return!1;l=!1}if(f&&t.skipSpaces(p)>=t.eMarks[c])return!1;if(n)return!0;const d=t.src.charCodeAt(p-1),_=t.tokens.length;l?(u=t.push("ordered_list_open","ol",1),1!==h&&(u.attrs=[["start",h]])):u=t.push("bullet_list_open","ul",1);const m=[c,0];u.map=m,u.markup=String.fromCharCode(d);let g=!1;const k=t.md.block.ruler.getRules("list"),D=t.parentType;for(t.parentType="list";c<r;){i=p,s=t.eMarks[c];const e=t.sCount[c]+p-(t.bMarks[c]+t.tShift[c]);let n=e;for(;i<s;){const e=t.src.charCodeAt(i);if(9===e)n+=4-(n+t.bsCount[c])%4;else{if(32!==e)break;n++}i++}const h=i;let f;f=h>=s?1:n-e,f>4&&(f=1);const _=e+f;u=t.push("list_item_open","li",1),u.markup=String.fromCharCode(d);const m=[c,0];u.map=m,l&&(u.info=t.src.slice(o,p-1));const D=t.tight,C=t.tShift[c],y=t.sCount[c],E=t.listIndent;if(t.listIndent=t.blkIndent,t.blkIndent=_,t.tight=!0,t.tShift[c]=h-t.bMarks[c],t.sCount[c]=n,h>=s&&t.isEmpty(c+1)?t.line=Math.min(t.line+2,r):t.md.block.tokenize(t,c,r,!0),t.tight&&!g||(a=!1),g=t.line-c>1&&t.isEmpty(t.line-1),t.blkIndent=t.listIndent,t.listIndent=E,t.tShift[c]=C,t.sCount[c]=y,t.tight=D,u=t.push("list_item_close","li",-1),u.markup=String.fromCharCode(d),c=t.line,m[1]=c,c>=r)break;if(t.sCount[c]<t.blkIndent)break;if(t.sCount[c]-t.blkIndent>=4)break;let A=!1;for(let e=0,n=k.length;e<n;e++)if(k[e](t,c,r,!0)){A=!0;break}if(A)break;if(l){if(p=Nt(t,c),p<0)break;o=t.bMarks[c]+t.tShift[c]}else if(p=Rt(t,c),p<0)break;if(d!==t.src.charCodeAt(p-1))break}return u=l?t.push("ordered_list_close","ol",-1):t.push("bullet_list_close","ul",-1),u.markup=String.fromCharCode(d),m[1]=c,t.line=c,t.parentType=D,a&&function(t,e){const r=t.level+2;for(let n=e+2,s=t.tokens.length-2;n<s;n++)t.tokens[n].level===r&&"paragraph_open"===t.tokens[n].type&&(t.tokens[n+2].hidden=!0,t.tokens[n].hidden=!0,n+=2)}(t,_),!0},["paragraph","reference","blockquote"]],["reference",function(t,e,r,n){let s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e],o=e+1;if(t.sCount[e]-t.blkIndent>=4)return!1;if(91!==t.src.charCodeAt(s))return!1;function u(e){const r=t.lineMax;if(e>=r||t.isEmpty(e))return null;let n=!1;if(t.sCount[e]-t.blkIndent>3&&(n=!0),t.sCount[e]<0&&(n=!0),!n){const n=t.md.block.ruler.getRules("reference"),s=t.parentType;t.parentType="reference";let i=!1;for(let s=0,o=n.length;s<o;s++)if(n[s](t,e,r,!0)){i=!0;break}if(t.parentType=s,i)return null}const s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e];return t.src.slice(s,i+1)}let c=t.src.slice(s,i+1);i=c.length;let a=-1;for(s=1;s<i;s++){const t=c.charCodeAt(s);if(91===t)return!1;if(93===t){a=s;break}if(10===t){const t=u(o);null!==t&&(c+=t,i=c.length,o++)}else if(92===t&&(s++,s<i&&10===c.charCodeAt(s))){const t=u(o);null!==t&&(c+=t,i=c.length,o++)}}if(a<0||58!==c.charCodeAt(a+1))return!1;for(s=a+2;s<i;s++){const t=c.charCodeAt(s);if(10===t){const t=u(o);null!==t&&(c+=t,i=c.length,o++)}else if(!st(t))break}const l=t.md.helpers.parseLinkDestination(c,s,i);if(!l.ok)return!1;const h=t.md.normalizeLink(l.str);if(!t.md.validateLink(h))return!1;s=l.pos;const p=s,f=o,d=s;for(;s<i;s++){const t=c.charCodeAt(s);if(10===t){const t=u(o);null!==t&&(c+=t,i=c.length,o++)}else if(!st(t))break}let _,m=t.md.helpers.parseLinkTitle(c,s,i);for(;m.can_continue;){const e=u(o);if(null===e)break;c+=e,s=i,i=c.length,o++,m=t.md.helpers.parseLinkTitle(c,s,i,m)}for(s<i&&d!==s&&m.ok?(_=m.str,s=m.pos):(_="",s=p,o=f);s<i;){if(!st(c.charCodeAt(s)))break;s++}if(s<i&&10!==c.charCodeAt(s)&&_)for(_="",s=p,o=f;s<i;){if(!st(c.charCodeAt(s)))break;s++}if(s<i&&10!==c.charCodeAt(s))return!1;const g=ct(c.slice(1,a));return!!g&&(n||(void 0===t.env.references&&(t.env.references={}),void 0===t.env.references[g]&&(t.env.references[g]={title:_,href:h}),t.line=o),!0)}],["html_block",function(t,e,r,n){let s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e];if(t.sCount[e]-t.blkIndent>=4)return!1;if(!t.md.options.html)return!1;if(60!==t.src.charCodeAt(s))return!1;let o=t.src.slice(s,i),u=0;for(;u<$t.length&&!$t[u][0].test(o);u++);if(u===$t.length)return!1;if(n)return $t[u][2];let c=e+1;if(!$t[u][1].test(o))for(;c<r&&!(t.sCount[c]<t.blkIndent);c++)if(s=t.bMarks[c]+t.tShift[c],i=t.eMarks[c],o=t.src.slice(s,i),$t[u][1].test(o)){0!==o.length&&c++;break}t.line=c;const a=t.push("html_block","",0);return a.map=[e,c],a.content=t.getLines(e,c,t.blkIndent,!0),!0},["paragraph","reference","blockquote"]],["heading",function(t,e,r,n){let s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e];if(t.sCount[e]-t.blkIndent>=4)return!1;let o=t.src.charCodeAt(s);if(35!==o||s>=i)return!1;let u=1;for(o=t.src.charCodeAt(++s);35===o&&s<i&&u<=6;)u++,o=t.src.charCodeAt(++s);if(u>6||s<i&&!st(o))return!1;if(n)return!0;i=t.skipSpacesBack(i,s);const c=t.skipCharsBack(i,35,s);c>s&&st(t.src.charCodeAt(c-1))&&(i=c),t.line=e+1;const a=t.push("heading_open","h"+String(u),1);a.markup="########".slice(0,u),a.map=[e,t.line];const l=t.push("inline","",0);return l.content=t.src.slice(s,i).trim(),l.map=[e,t.line],l.children=[],t.push("heading_close","h"+String(u),-1).markup="########".slice(0,u),!0},["paragraph","reference","blockquote"]],["lheading",function(t,e,r){const n=t.md.block.ruler.getRules("paragraph");if(t.sCount[e]-t.blkIndent>=4)return!1;const s=t.parentType;t.parentType="paragraph";let i,o=0,u=e+1;for(;u<r&&!t.isEmpty(u);u++){if(t.sCount[u]-t.blkIndent>3)continue;if(t.sCount[u]>=t.blkIndent){let e=t.bMarks[u]+t.tShift[u];const r=t.eMarks[u];if(e<r&&(i=t.src.charCodeAt(e),(45===i||61===i)&&(e=t.skipChars(e,i),e=t.skipSpaces(e),e>=r))){o=61===i?1:2;break}}if(t.sCount[u]<0)continue;let e=!1;for(let s=0,i=n.length;s<i;s++)if(n[s](t,u,r,!0)){e=!0;break}if(e)break}if(!o)return!1;const c=t.getLines(e,u,t.blkIndent,!1).trim();t.line=u+1;const a=t.push("heading_open","h"+String(o),1);a.markup=String.fromCharCode(i),a.map=[e,t.line];const l=t.push("inline","",0);return l.content=c,l.map=[e,t.line-1],l.children=[],t.push("heading_close","h"+String(o),-1).markup=String.fromCharCode(i),t.parentType=s,!0}],["paragraph",function(t,e,r){const n=t.md.block.ruler.getRules("paragraph"),s=t.parentType;let i=e+1;for(t.parentType="paragraph";i<r&&!t.isEmpty(i);i++){if(t.sCount[i]-t.blkIndent>3)continue;if(t.sCount[i]<0)continue;let e=!1;for(let s=0,o=n.length;s<o;s++)if(n[s](t,i,r,!0)){e=!0;break}if(e)break}const o=t.getLines(e,i,t.blkIndent,!1).trim();t.line=i,t.push("paragraph_open","p",1).map=[e,t.line];const u=t.push("inline","",0);return u.content=o,u.map=[e,t.line],u.children=[],t.push("paragraph_close","p",-1),t.parentType=s,!0}]];function Ht(){this.ruler=new dt;for(let t=0;t<Ut.length;t++)this.ruler.push(Ut[t][0],Ut[t][1],{alt:(Ut[t][2]||[]).slice()})}function Vt(t,e,r,n){this.src=t,this.env=r,this.md=e,this.tokens=n,this.tokens_meta=Array(n.length),this.pos=0,this.posMax=this.src.length,this.level=0,this.pending="",this.pendingLevel=0,this.cache={},this.delimiters=[],this._prev_delimiters=[],this.backticks={},this.backticksScanned=!1,this.linkLevel=0}function Gt(t){switch(t){case 10:case 33:case 35:case 36:case 37:case 38:case 42:case 43:case 45:case 58:case 60:case 61:case 62:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 125:case 126:return!0;default:return!1}}Ht.prototype.tokenize=function(t,e,r){const n=this.ruler.getRules(""),s=n.length,i=t.md.options.maxNesting;let o=e,u=!1;for(;o<r&&(t.line=o=t.skipEmptyLines(o),!(o>=r))&&!(t.sCount[o]<t.blkIndent);){if(t.level>=i){t.line=r;break}const e=t.line;let c=!1;for(let i=0;i<s;i++)if(c=n[i](t,o,r,!1),c){if(e>=t.line)throw new Error("block rule didn't increment state.line");break}if(!c)throw new Error("none of the block rules matched");t.tight=!u,t.isEmpty(t.line-1)&&(u=!0),o=t.line,o<r&&t.isEmpty(o)&&(u=!0,o++,t.line=o)}},Ht.prototype.parse=function(t,e,r,n){if(!t)return;const s=new this.State(t,e,r,n);this.tokenize(s,s.line,s.lineMax)},Ht.prototype.State=It,Vt.prototype.pushPending=function(){const t=new _t("text","",0);return t.content=this.pending,t.level=this.pendingLevel,this.tokens.push(t),this.pending="",t},Vt.prototype.push=function(t,e,r){this.pending&&this.pushPending();const n=new _t(t,e,r);let s=null;return r<0&&(this.level--,this.delimiters=this._prev_delimiters.pop()),n.level=this.level,r>0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],s={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(n),this.tokens_meta.push(s),n},Vt.prototype.scanDelims=function(t,e){const r=this.posMax,n=this.src.charCodeAt(t),s=t>0?this.src.charCodeAt(t-1):32;let i=t;for(;i<r&&this.src.charCodeAt(i)===n;)i++;const o=i-t,u=i<r?this.src.charCodeAt(i):32,c=ut(s)||ot(String.fromCharCode(s)),a=ut(u)||ot(String.fromCharCode(u)),l=it(s),h=it(u),p=!h&&(!a||l||c),f=!l&&(!c||h||a);return{can_open:p&&(e||!f||c),can_close:f&&(e||!p||a),length:o}},Vt.prototype.Token=_t;const Wt=/(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$/i;const Jt=[];for(let t=0;t<256;t++)Jt.push(0);function Qt(t,e){let r;const n=[],s=e.length;for(let i=0;i<s;i++){const s=e[i];if(126!==s.marker)continue;if(-1===s.end)continue;const o=e[s.end];r=t.tokens[s.token],r.type="s_open",r.tag="s",r.nesting=1,r.markup="~~",r.content="",r=t.tokens[o.token],r.type="s_close",r.tag="s",r.nesting=-1,r.markup="~~",r.content="","text"===t.tokens[o.token-1].type&&"~"===t.tokens[o.token-1].content&&n.push(o.token-1)}for(;n.length;){const e=n.pop();let s=e+1;for(;s<t.tokens.length&&"s_close"===t.tokens[s].type;)s++;s--,e!==s&&(r=t.tokens[s],t.tokens[s]=t.tokens[e],t.tokens[e]=r)}}"\\!\"#$%&'()*+,./:;<=>?@[]^_`{|}~-".split("").forEach((function(t){Jt[t.charCodeAt(0)]=1}));var Xt={tokenize:function(t,e){const r=t.pos,n=t.src.charCodeAt(r);if(e)return!1;if(126!==n)return!1;const s=t.scanDelims(t.pos,!0);let i=s.length;const o=String.fromCharCode(n);if(i<2)return!1;let u;i%2&&(u=t.push("text","",0),u.content=o,i--);for(let e=0;e<i;e+=2)u=t.push("text","",0),u.content=o+o,t.delimiters.push({marker:n,length:0,token:t.tokens.length-1,end:-1,open:s.can_open,close:s.can_close});return t.pos+=s.length,!0},postProcess:function(t){const e=t.tokens_meta,r=t.tokens_meta.length;Qt(t,t.delimiters);for(let n=0;n<r;n++)e[n]&&e[n].delimiters&&Qt(t,e[n].delimiters)}};function Yt(t,e){for(let r=e.length-1;r>=0;r--){const n=e[r];if(95!==n.marker&&42!==n.marker)continue;if(-1===n.end)continue;const s=e[n.end],i=r>0&&e[r-1].end===n.end+1&&e[r-1].marker===n.marker&&e[r-1].token===n.token-1&&e[n.end+1].token===s.token+1,o=String.fromCharCode(n.marker),u=t.tokens[n.token];u.type=i?"strong_open":"em_open",u.tag=i?"strong":"em",u.nesting=1,u.markup=i?o+o:o,u.content="";const c=t.tokens[s.token];c.type=i?"strong_close":"em_close",c.tag=i?"strong":"em",c.nesting=-1,c.markup=i?o+o:o,c.content="",i&&(t.tokens[e[r-1].token].content="",t.tokens[e[n.end+1].token].content="",r--)}}var Kt={tokenize:function(t,e){const r=t.pos,n=t.src.charCodeAt(r);if(e)return!1;if(95!==n&&42!==n)return!1;const s=t.scanDelims(t.pos,42===n);for(let e=0;e<s.length;e++){t.push("text","",0).content=String.fromCharCode(n),t.delimiters.push({marker:n,length:s.length,token:t.tokens.length-1,end:-1,open:s.can_open,close:s.can_close})}return t.pos+=s.length,!0},postProcess:function(t){const e=t.tokens_meta,r=t.tokens_meta.length;Yt(t,t.delimiters);for(let n=0;n<r;n++)e[n]&&e[n].delimiters&&Yt(t,e[n].delimiters)}};const te=/^([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$/,ee=/^([a-zA-Z][a-zA-Z0-9+.-]{1,31}):([^<>\x00-\x20]*)$/;const re=/^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,ne=/^&([a-z][a-z0-9]{1,31});/i;function se(t){const e={},r=t.length;if(!r)return;let n=0,s=-2;const i=[];for(let o=0;o<r;o++){const r=t[o];if(i.push(0),t[n].marker===r.marker&&s===r.token-1||(n=o),s=r.token,r.length=r.length||0,!r.close)continue;e.hasOwnProperty(r.marker)||(e[r.marker]=[-1,-1,-1,-1,-1,-1]);const u=e[r.marker][(r.open?3:0)+r.length%3];let c=n-i[n]-1,a=c;for(;c>u;c-=i[c]+1){const e=t[c];if(e.marker===r.marker&&(e.open&&e.end<0)){let n=!1;if((e.close||r.open)&&(e.length+r.length)%3==0&&(e.length%3==0&&r.length%3==0||(n=!0)),!n){const n=c>0&&!t[c-1].open?i[c-1]+1:0;i[o]=o-c+n,i[c]=n,r.open=!1,e.end=o,e.close=!1,a=-1,s=-2;break}}}-1!==a&&(e[r.marker][(r.open?3:0)+(r.length||0)%3]=a)}}const ie=[["text",function(t,e){let r=t.pos;for(;r<t.posMax&&!Gt(t.src.charCodeAt(r));)r++;return r!==t.pos&&(e||(t.pending+=t.src.slice(t.pos,r)),t.pos=r,!0)}],["linkify",function(t,e){if(!t.md.options.linkify)return!1;if(t.linkLevel>0)return!1;const r=t.pos;if(r+3>t.posMax)return!1;if(58!==t.src.charCodeAt(r))return!1;if(47!==t.src.charCodeAt(r+1))return!1;if(47!==t.src.charCodeAt(r+2))return!1;const n=t.pending.match(Wt);if(!n)return!1;const s=n[1],i=t.md.linkify.matchAtStart(t.src.slice(r-s.length));if(!i)return!1;let o=i.url;if(o.length<=s.length)return!1;o=o.replace(/\*+$/,"");const u=t.md.normalizeLink(o);if(!t.md.validateLink(u))return!1;if(!e){t.pending=t.pending.slice(0,-s.length);const e=t.push("link_open","a",1);e.attrs=[["href",u]],e.markup="linkify",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(o);const r=t.push("link_close","a",-1);r.markup="linkify",r.info="auto"}return t.pos+=o.length-s.length,!0}],["newline",function(t,e){let r=t.pos;if(10!==t.src.charCodeAt(r))return!1;const n=t.pending.length-1,s=t.posMax;if(!e)if(n>=0&&32===t.pending.charCodeAt(n))if(n>=1&&32===t.pending.charCodeAt(n-1)){let e=n-1;for(;e>=1&&32===t.pending.charCodeAt(e-1);)e--;t.pending=t.pending.slice(0,e),t.push("hardbreak","br",0)}else t.pending=t.pending.slice(0,-1),t.push("softbreak","br",0);else t.push("softbreak","br",0);for(r++;r<s&&st(t.src.charCodeAt(r));)r++;return t.pos=r,!0}],["escape",function(t,e){let r=t.pos;const n=t.posMax;if(92!==t.src.charCodeAt(r))return!1;if(r++,r>=n)return!1;let s=t.src.charCodeAt(r);if(10===s){for(e||t.push("hardbreak","br",0),r++;r<n&&(s=t.src.charCodeAt(r),st(s));)r++;return t.pos=r,!0}let i=t.src[r];if(s>=55296&&s<=56319&&r+1<n){const e=t.src.charCodeAt(r+1);e>=56320&&e<=57343&&(i+=t.src[r+1],r++)}const o="\\"+i;if(!e){const e=t.push("text_special","",0);s<256&&0!==Jt[s]?e.content=i:e.content=o,e.markup=o,e.info="escape"}return t.pos=r+1,!0}],["backticks",function(t,e){let r=t.pos;if(96!==t.src.charCodeAt(r))return!1;const n=r;r++;const s=t.posMax;for(;r<s&&96===t.src.charCodeAt(r);)r++;const i=t.src.slice(n,r),o=i.length;if(t.backticksScanned&&(t.backticks[o]||0)<=n)return e||(t.pending+=i),t.pos+=o,!0;let u,c=r;for(;-1!==(u=t.src.indexOf("`",c));){for(c=u+1;c<s&&96===t.src.charCodeAt(c);)c++;const n=c-u;if(n===o){if(!e){const e=t.push("code_inline","code",0);e.markup=i,e.content=t.src.slice(r,u).replace(/\n/g," ").replace(/^ (.+) $/,"$1")}return t.pos=c,!0}t.backticks[n]=u}return t.backticksScanned=!0,e||(t.pending+=i),t.pos+=o,!0}],["strikethrough",Xt.tokenize],["emphasis",Kt.tokenize],["link",function(t,e){let r,n,s,i,o="",u="",c=t.pos,a=!0;if(91!==t.src.charCodeAt(t.pos))return!1;const l=t.pos,h=t.posMax,p=t.pos+1,f=t.md.helpers.parseLinkLabel(t,t.pos,!0);if(f<0)return!1;let d=f+1;if(d<h&&40===t.src.charCodeAt(d)){for(a=!1,d++;d<h&&(r=t.src.charCodeAt(d),st(r)||10===r);d++);if(d>=h)return!1;if(c=d,s=t.md.helpers.parseLinkDestination(t.src,d,t.posMax),s.ok){for(o=t.md.normalizeLink(s.str),t.md.validateLink(o)?d=s.pos:o="",c=d;d<h&&(r=t.src.charCodeAt(d),st(r)||10===r);d++);if(s=t.md.helpers.parseLinkTitle(t.src,d,t.posMax),d<h&&c!==d&&s.ok)for(u=s.str,d=s.pos;d<h&&(r=t.src.charCodeAt(d),st(r)||10===r);d++);}(d>=h||41!==t.src.charCodeAt(d))&&(a=!0),d++}if(a){if(void 0===t.env.references)return!1;if(d<h&&91===t.src.charCodeAt(d)?(c=d+1,d=t.md.helpers.parseLinkLabel(t,d),d>=0?n=t.src.slice(c,d++):d=f+1):d=f+1,n||(n=t.src.slice(p,f)),i=t.env.references[ct(n)],!i)return t.pos=l,!1;o=i.href,u=i.title}if(!e){t.pos=p,t.posMax=f;const e=[["href",o]];t.push("link_open","a",1).attrs=e,u&&e.push(["title",u]),t.linkLevel++,t.md.inline.tokenize(t),t.linkLevel--,t.push("link_close","a",-1)}return t.pos=d,t.posMax=h,!0}],["image",function(t,e){let r,n,s,i,o,u,c,a,l="";const h=t.pos,p=t.posMax;if(33!==t.src.charCodeAt(t.pos))return!1;if(91!==t.src.charCodeAt(t.pos+1))return!1;const f=t.pos+2,d=t.md.helpers.parseLinkLabel(t,t.pos+1,!1);if(d<0)return!1;if(i=d+1,i<p&&40===t.src.charCodeAt(i)){for(i++;i<p&&(r=t.src.charCodeAt(i),st(r)||10===r);i++);if(i>=p)return!1;for(a=i,u=t.md.helpers.parseLinkDestination(t.src,i,t.posMax),u.ok&&(l=t.md.normalizeLink(u.str),t.md.validateLink(l)?i=u.pos:l=""),a=i;i<p&&(r=t.src.charCodeAt(i),st(r)||10===r);i++);if(u=t.md.helpers.parseLinkTitle(t.src,i,t.posMax),i<p&&a!==i&&u.ok)for(c=u.str,i=u.pos;i<p&&(r=t.src.charCodeAt(i),st(r)||10===r);i++);else c="";if(i>=p||41!==t.src.charCodeAt(i))return t.pos=h,!1;i++}else{if(void 0===t.env.references)return!1;if(i<p&&91===t.src.charCodeAt(i)?(a=i+1,i=t.md.helpers.parseLinkLabel(t,i),i>=0?s=t.src.slice(a,i++):i=d+1):i=d+1,s||(s=t.src.slice(f,d)),o=t.env.references[ct(s)],!o)return t.pos=h,!1;l=o.href,c=o.title}if(!e){n=t.src.slice(f,d);const e=[];t.md.inline.parse(n,t.md,t.env,e);const r=t.push("image","img",0),s=[["src",l],["alt",""]];r.attrs=s,r.children=e,r.content=n,c&&s.push(["title",c])}return t.pos=i,t.posMax=p,!0}],["autolink",function(t,e){let r=t.pos;if(60!==t.src.charCodeAt(r))return!1;const n=t.pos,s=t.posMax;for(;;){if(++r>=s)return!1;const e=t.src.charCodeAt(r);if(60===e)return!1;if(62===e)break}const i=t.src.slice(n+1,r);if(ee.test(i)){const r=t.md.normalizeLink(i);if(!t.md.validateLink(r))return!1;if(!e){const e=t.push("link_open","a",1);e.attrs=[["href",r]],e.markup="autolink",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(i);const n=t.push("link_close","a",-1);n.markup="autolink",n.info="auto"}return t.pos+=i.length+2,!0}if(te.test(i)){const r=t.md.normalizeLink("mailto:"+i);if(!t.md.validateLink(r))return!1;if(!e){const e=t.push("link_open","a",1);e.attrs=[["href",r]],e.markup="autolink",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(i);const n=t.push("link_close","a",-1);n.markup="autolink",n.info="auto"}return t.pos+=i.length+2,!0}return!1}],["html_inline",function(t,e){if(!t.md.options.html)return!1;const r=t.posMax,n=t.pos;if(60!==t.src.charCodeAt(n)||n+2>=r)return!1;const s=t.src.charCodeAt(n+1);if(33!==s&&63!==s&&47!==s&&!function(t){const e=32|t;return e>=97&&e<=122}(s))return!1;const i=t.src.slice(n).match(jt);if(!i)return!1;if(!e){const e=t.push("html_inline","",0);e.content=i[0],o=e.content,/^<a[>\s]/i.test(o)&&t.linkLevel++,function(t){return/^<\/a\s*>/i.test(t)}(e.content)&&t.linkLevel--}var o;return t.pos+=i[0].length,!0}],["entity",function(t,e){const r=t.pos,n=t.posMax;if(38!==t.src.charCodeAt(r))return!1;if(r+1>=n)return!1;if(35===t.src.charCodeAt(r+1)){const n=t.src.slice(r).match(re);if(n){if(!e){const e="x"===n[1][0].toLowerCase()?parseInt(n[1].slice(1),16):parseInt(n[1],10),r=t.push("text_special","",0);r.content=V(e)?G(e):G(65533),r.markup=n[0],r.info="entity"}return t.pos+=n[0].length,!0}}else{const n=t.src.slice(r).match(ne);if(n){const r=j(n[0]);if(r!==n[0]){if(!e){const e=t.push("text_special","",0);e.content=r,e.markup=n[0],e.info="entity"}return t.pos+=n[0].length,!0}}}return!1}]],oe=[["balance_pairs",function(t){const e=t.tokens_meta,r=t.tokens_meta.length;se(t.delimiters);for(let t=0;t<r;t++)e[t]&&e[t].delimiters&&se(e[t].delimiters)}],["strikethrough",Xt.postProcess],["emphasis",Kt.postProcess],["fragments_join",function(t){let e,r,n=0;const s=t.tokens,i=t.tokens.length;for(e=r=0;e<i;e++)s[e].nesting<0&&n--,s[e].level=n,s[e].nesting>0&&n++,"text"===s[e].type&&e+1<i&&"text"===s[e+1].type?s[e+1].content=s[e].content+s[e+1].content:(e!==r&&(s[r]=s[e]),r++);e!==r&&(s.length=r)}]];function ue(){this.ruler=new dt;for(let t=0;t<ie.length;t++)this.ruler.push(ie[t][0],ie[t][1]);this.ruler2=new dt;for(let t=0;t<oe.length;t++)this.ruler2.push(oe[t][0],oe[t][1])}function ce(t){return Array.prototype.slice.call(arguments,1).forEach((function(e){e&&Object.keys(e).forEach((function(r){t[r]=e[r]}))})),t}function ae(t){return Object.prototype.toString.call(t)}function le(t){return"[object Function]"===ae(t)}function he(t){return t.replace(/[.?*+^$[\]\\(){}|-]/g,"\\$&")}ue.prototype.skipToken=function(t){const e=t.pos,r=this.ruler.getRules(""),n=r.length,s=t.md.options.maxNesting,i=t.cache;if(void 0!==i[e])return void(t.pos=i[e]);let o=!1;if(t.level<s){for(let s=0;s<n;s++)if(t.level++,o=r[s](t,!0),t.level--,o){if(e>=t.pos)throw new Error("inline rule didn't increment state.pos");break}}else t.pos=t.posMax;o||t.pos++,i[e]=t.pos},ue.prototype.tokenize=function(t){const e=this.ruler.getRules(""),r=e.length,n=t.posMax,s=t.md.options.maxNesting;for(;t.pos<n;){const i=t.pos;let o=!1;if(t.level<s)for(let n=0;n<r;n++)if(o=e[n](t,!1),o){if(i>=t.pos)throw new Error("inline rule didn't increment state.pos");break}if(o){if(t.pos>=n)break}else t.pending+=t.src[t.pos++]}t.pending&&t.pushPending()},ue.prototype.parse=function(t,e,r,n){const s=new this.State(t,e,r,n);this.tokenize(s);const i=this.ruler2.getRules(""),o=i.length;for(let t=0;t<o;t++)i[t](s)},ue.prototype.State=Vt;const pe={fuzzyLink:!0,fuzzyEmail:!0,fuzzyIP:!1};const fe={"http:":{validate:function(t,e,r){const n=t.slice(e);return r.re.http||(r.re.http=new RegExp("^\\/\\/"+r.re.src_auth+r.re.src_host_port_strict+r.re.src_path,"i")),r.re.http.test(n)?n.match(r.re.http)[0].length:0}},"https:":"http:","ftp:":"http:","//":{validate:function(t,e,r){const n=t.slice(e);return r.re.no_http||(r.re.no_http=new RegExp("^"+r.re.src_auth+"(?:localhost|(?:(?:"+r.re.src_domain+")\\.)+"+r.re.src_domain_root+")"+r.re.src_port+r.re.src_host_terminator+r.re.src_path,"i")),r.re.no_http.test(n)?e>=3&&":"===t[e-3]||e>=3&&"/"===t[e-3]?0:n.match(r.re.no_http)[0].length:0}},"mailto:":{validate:function(t,e,r){const n=t.slice(e);return r.re.mailto||(r.re.mailto=new RegExp("^"+r.re.src_email_name+"@"+r.re.src_host_strict,"i")),r.re.mailto.test(n)?n.match(r.re.mailto)[0].length:0}}},de="a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]",_e="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|\u0440\u0444".split("|");function me(t){const e=t.re=function(t){const e={};t=t||{},e.src_Any=C.source,e.src_Cc=y.source,e.src_Z=b.source,e.src_P=E.source,e.src_ZPCc=[e.src_Z,e.src_P,e.src_Cc].join("|"),e.src_ZCc=[e.src_Z,e.src_Cc].join("|");const r="[><\uff5c]";return e.src_pseudo_letter="(?:(?![><\uff5c]|"+e.src_ZPCc+")"+e.src_Any+")",e.src_ip4="(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",e.src_auth="(?:(?:(?!"+e.src_ZCc+"|[@/\\[\\]()]).)+@)?",e.src_port="(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?",e.src_host_terminator="(?=$|[><\uff5c]|"+e.src_ZPCc+")(?!"+(t["---"]?"-(?!--)|":"-|")+"_|:\\d|\\.-|\\.(?!$|"+e.src_ZPCc+"))",e.src_path="(?:[/?#](?:(?!"+e.src_ZCc+"|"+r+"|[()[\\]{}.,\"'?!\\-;]).|\\[(?:(?!"+e.src_ZCc+"|\\]).)*\\]|\\((?:(?!"+e.src_ZCc+"|[)]).)*\\)|\\{(?:(?!"+e.src_ZCc+'|[}]).)*\\}|\\"(?:(?!'+e.src_ZCc+'|["]).)+\\"|\\\'(?:(?!'+e.src_ZCc+"|[']).)+\\'|\\'(?="+e.src_pseudo_letter+"|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!"+e.src_ZCc+"|[.]|$)|"+(t["---"]?"\\-(?!--(?:[^-]|$))(?:-*)|":"\\-+|")+",(?!"+e.src_ZCc+"|$)|;(?!"+e.src_ZCc+"|$)|\\!+(?!"+e.src_ZCc+"|[!]|$)|\\?(?!"+e.src_ZCc+"|[?]|$))+|\\/)?",e.src_email_name='[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*',e.src_xn="xn--[a-z0-9\\-]{1,59}",e.src_domain_root="(?:"+e.src_xn+"|"+e.src_pseudo_letter+"{1,63})",e.src_domain="(?:"+e.src_xn+"|(?:"+e.src_pseudo_letter+")|(?:"+e.src_pseudo_letter+"(?:-|"+e.src_pseudo_letter+"){0,61}"+e.src_pseudo_letter+"))",e.src_host="(?:(?:(?:(?:"+e.src_domain+")\\.)*"+e.src_domain+"))",e.tpl_host_fuzzy="(?:"+e.src_ip4+"|(?:(?:(?:"+e.src_domain+")\\.)+(?:%TLDS%)))",e.tpl_host_no_ip_fuzzy="(?:(?:(?:"+e.src_domain+")\\.)+(?:%TLDS%))",e.src_host_strict=e.src_host+e.src_host_terminator,e.tpl_host_fuzzy_strict=e.tpl_host_fuzzy+e.src_host_terminator,e.src_host_port_strict=e.src_host+e.src_port+e.src_host_terminator,e.tpl_host_port_fuzzy_strict=e.tpl_host_fuzzy+e.src_port+e.src_host_terminator,e.tpl_host_port_no_ip_fuzzy_strict=e.tpl_host_no_ip_fuzzy+e.src_port+e.src_host_terminator,e.tpl_host_fuzzy_test="localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:"+e.src_ZPCc+"|>|$))",e.tpl_email_fuzzy='(^|[><\uff5c]|"|\\(|'+e.src_ZCc+")("+e.src_email_name+"@"+e.tpl_host_fuzzy_strict+")",e.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|"+e.src_ZPCc+"))((?![$+<=>^`|\uff5c])"+e.tpl_host_port_fuzzy_strict+e.src_path+")",e.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|"+e.src_ZPCc+"))((?![$+<=>^`|\uff5c])"+e.tpl_host_port_no_ip_fuzzy_strict+e.src_path+")",e}(t.__opts__),r=t.__tlds__.slice();function n(t){return t.replace("%TLDS%",e.src_tlds)}t.onCompile(),t.__tlds_replaced__||r.push(de),r.push(e.src_xn),e.src_tlds=r.join("|"),e.email_fuzzy=RegExp(n(e.tpl_email_fuzzy),"i"),e.link_fuzzy=RegExp(n(e.tpl_link_fuzzy),"i"),e.link_no_ip_fuzzy=RegExp(n(e.tpl_link_no_ip_fuzzy),"i"),e.host_fuzzy_test=RegExp(n(e.tpl_host_fuzzy_test),"i");const s=[];function i(t,e){throw new Error('(LinkifyIt) Invalid schema "'+t+'": '+e)}t.__compiled__={},Object.keys(t.__schemas__).forEach((function(e){const r=t.__schemas__[e];if(null===r)return;const n={validate:null,link:null};if(t.__compiled__[e]=n,"[object Object]"===ae(r))return!function(t){return"[object RegExp]"===ae(t)}(r.validate)?le(r.validate)?n.validate=r.validate:i(e,r):n.validate=function(t){return function(e,r){const n=e.slice(r);return t.test(n)?n.match(t)[0].length:0}}(r.validate),void(le(r.normalize)?n.normalize=r.normalize:r.normalize?i(e,r):n.normalize=function(t,e){e.normalize(t)});!function(t){return"[object String]"===ae(t)}(r)?i(e,r):s.push(e)})),s.forEach((function(e){t.__compiled__[t.__schemas__[e]]&&(t.__compiled__[e].validate=t.__compiled__[t.__schemas__[e]].validate,t.__compiled__[e].normalize=t.__compiled__[t.__schemas__[e]].normalize)})),t.__compiled__[""]={validate:null,normalize:function(t,e){e.normalize(t)}};const o=Object.keys(t.__compiled__).filter((function(e){return e.length>0&&t.__compiled__[e]})).map(he).join("|");t.re.schema_test=RegExp("(^|(?!_)(?:[><\uff5c]|"+e.src_ZPCc+"))("+o+")","i"),t.re.schema_search=RegExp("(^|(?!_)(?:[><\uff5c]|"+e.src_ZPCc+"))("+o+")","ig"),t.re.schema_at_start=RegExp("^"+t.re.schema_search.source,"i"),t.re.pretest=RegExp("("+t.re.schema_test.source+")|("+t.re.host_fuzzy_test.source+")|@","i"),function(t){t.__index__=-1,t.__text_cache__=""}(t)}function ge(t,e){const r=t.__index__,n=t.__last_index__,s=t.__text_cache__.slice(r,n);this.schema=t.__schema__.toLowerCase(),this.index=r+e,this.lastIndex=n+e,this.raw=s,this.text=s,this.url=s}function ke(t,e){const r=new ge(t,e);return t.__compiled__[r.schema].normalize(r,t),r}function De(t,e){if(!(this instanceof De))return new De(t,e);var r;e||(r=t,Object.keys(r||{}).reduce((function(t,e){return t||pe.hasOwnProperty(e)}),!1)&&(e=t,t={})),this.__opts__=ce({},pe,e),this.__index__=-1,this.__last_index__=-1,this.__schema__="",this.__text_cache__="",this.__schemas__=ce({},fe,t),this.__compiled__={},this.__tlds__=_e,this.__tlds_replaced__=!1,this.re={},me(this)}De.prototype.add=function(t,e){return this.__schemas__[t]=e,me(this),this},De.prototype.set=function(t){return this.__opts__=ce(this.__opts__,t),this},De.prototype.test=function(t){if(this.__text_cache__=t,this.__index__=-1,!t.length)return!1;let e,r,n,s,i,o,u,c,a;if(this.re.schema_test.test(t))for(u=this.re.schema_search,u.lastIndex=0;null!==(e=u.exec(t));)if(s=this.testSchemaAt(t,e[2],u.lastIndex),s){this.__schema__=e[2],this.__index__=e.index+e[1].length,this.__last_index__=e.index+e[0].length+s;break}return this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&(c=t.search(this.re.host_fuzzy_test),c>=0&&(this.__index__<0||c<this.__index__)&&null!==(r=t.match(this.__opts__.fuzzyIP?this.re.link_fuzzy:this.re.link_no_ip_fuzzy))&&(i=r.index+r[1].length,(this.__index__<0||i<this.__index__)&&(this.__schema__="",this.__index__=i,this.__last_index__=r.index+r[0].length))),this.__opts__.fuzzyEmail&&this.__compiled__["mailto:"]&&(a=t.indexOf("@"),a>=0&&null!==(n=t.match(this.re.email_fuzzy))&&(i=n.index+n[1].length,o=n.index+n[0].length,(this.__index__<0||i<this.__index__||i===this.__index__&&o>this.__last_index__)&&(this.__schema__="mailto:",this.__index__=i,this.__last_index__=o))),this.__index__>=0},De.prototype.pretest=function(t){return this.re.pretest.test(t)},De.prototype.testSchemaAt=function(t,e,r){return this.__compiled__[e.toLowerCase()]?this.__compiled__[e.toLowerCase()].validate(t,r,this):0},De.prototype.match=function(t){const e=[];let r=0;this.__index__>=0&&this.__text_cache__===t&&(e.push(ke(this,r)),r=this.__last_index__);let n=r?t.slice(r):t;for(;this.test(n);)e.push(ke(this,r)),n=n.slice(this.__last_index__),r+=this.__last_index__;return e.length?e:null},De.prototype.matchAtStart=function(t){if(this.__text_cache__=t,this.__index__=-1,!t.length)return null;const e=this.re.schema_at_start.exec(t);if(!e)return null;const r=this.testSchemaAt(t,e[2],e[0].length);return r?(this.__schema__=e[2],this.__index__=e.index+e[1].length,this.__last_index__=e.index+e[0].length+r,ke(this,0)):null},De.prototype.tlds=function(t,e){return t=Array.isArray(t)?t:[t],e?(this.__tlds__=this.__tlds__.concat(t).sort().filter((function(t,e,r){return t!==r[e-1]})).reverse(),me(this),this):(this.__tlds__=t.slice(),this.__tlds_replaced__=!0,me(this),this)},De.prototype.normalize=function(t){t.schema||(t.url="http://"+t.url),"mailto:"!==t.schema||/^mailto:/i.test(t.url)||(t.url="mailto:"+t.url)},De.prototype.onCompile=function(){};const Ce=2147483647,ye=36,Ee=/^xn--/,Ae=/[^\0-\x7F]/,be=/[\x2E\u3002\uFF0E\uFF61]/g,Fe={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},xe=Math.floor,we=String.fromCharCode;function ve(t){throw new RangeError(Fe[t])}function ze(t,e){const r=t.split("@");let n="";r.length>1&&(n=r[0]+"@",t=r[1]);const s=function(t,e){const r=[];let n=t.length;for(;n--;)r[n]=e(t[n]);return r}((t=t.replace(be,".")).split("."),e).join(".");return n+s}function Se(t){const e=[];let r=0;const n=t.length;for(;r<n;){const s=t.charCodeAt(r++);if(s>=55296&&s<=56319&&r<n){const n=t.charCodeAt(r++);56320==(64512&n)?e.push(((1023&s)<<10)+(1023&n)+65536):(e.push(s),r--)}else e.push(s)}return e}const qe=function(t,e){return t+22+75*(t<26)-((0!=e)<<5)},Be=function(t,e,r){let n=0;for(t=r?xe(t/700):t>>1,t+=xe(t/e);t>455;n+=ye)t=xe(t/35);return xe(n+36*t/(t+38))},Le=function(t){const e=[],r=t.length;let n=0,s=128,i=72,o=t.lastIndexOf("-");o<0&&(o=0);for(let r=0;r<o;++r)t.charCodeAt(r)>=128&&ve("not-basic"),e.push(t.charCodeAt(r));for(let c=o>0?o+1:0;c<r;){const o=n;for(let e=1,s=ye;;s+=ye){c>=r&&ve("invalid-input");const o=(u=t.charCodeAt(c++))>=48&&u<58?u-48+26:u>=65&&u<91?u-65:u>=97&&u<123?u-97:ye;o>=ye&&ve("invalid-input"),o>xe((Ce-n)/e)&&ve("overflow"),n+=o*e;const a=s<=i?1:s>=i+26?26:s-i;if(o<a)break;const l=ye-a;e>xe(Ce/l)&&ve("overflow"),e*=l}const a=e.length+1;i=Be(n-o,a,0==o),xe(n/a)>Ce-s&&ve("overflow"),s+=xe(n/a),n%=a,e.splice(n++,0,s)}var u;return String.fromCodePoint(...e)},Ie=function(t){const e=[],r=(t=Se(t)).length;let n=128,s=0,i=72;for(const r of t)r<128&&e.push(we(r));const o=e.length;let u=o;for(o&&e.push("-");u<r;){let r=Ce;for(const e of t)e>=n&&e<r&&(r=e);const c=u+1;r-n>xe((Ce-s)/c)&&ve("overflow"),s+=(r-n)*c,n=r;for(const r of t)if(r<n&&++s>Ce&&ve("overflow"),r===n){let t=s;for(let r=ye;;r+=ye){const n=r<=i?1:r>=i+26?26:r-i;if(t<n)break;const s=t-n,o=ye-n;e.push(we(qe(n+s%o,0))),t=xe(s/o)}e.push(we(qe(t,0))),i=Be(s,c,u===o),s=0,++u}++s,++n}return e.join("")},Me={version:"2.3.1",ucs2:{decode:Se,encode:t=>String.fromCodePoint(...t)},decode:Le,encode:Ie,toASCII:function(t){return ze(t,(function(t){return Ae.test(t)?"xn--"+Ie(t):t}))},toUnicode:function(t){return ze(t,(function(t){return Ee.test(t)?Le(t.slice(4).toLowerCase()):t}))}};const Te={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:100},components:{core:{},block:{},inline:{}}},zero:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["paragraph"]},inline:{rules:["text"],rules2:["balance_pairs","fragments_join"]}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["blockquote","code","fence","heading","hr","html_block","lheading","list","reference","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","html_inline","image","link","newline","text"],rules2:["balance_pairs","emphasis","fragments_join"]}}}},Re=/^(vbscript|javascript|file|data):/,Ne=/^data:image\/(gif|png|jpeg|webp);/;function Pe(t){const e=t.trim().toLowerCase();return!Re.test(e)||Ne.test(e)}const Oe=["http:","https:","mailto:"];function je(t){const e=g(t,!0);if(e.hostname&&(!e.protocol||Oe.indexOf(e.protocol)>=0))try{e.hostname=Me.toASCII(e.hostname)}catch(t){}return n(s(e))}function Ze(t){const r=g(t,!0);if(r.hostname&&(!r.protocol||Oe.indexOf(r.protocol)>=0))try{r.hostname=Me.toUnicode(r.hostname)}catch(t){}return e(s(r),e.defaultChars+"%")}function $e(t,e){if(!(this instanceof $e))return new $e(t,e);e||Z(t)||(e=t||{},t="default"),this.inline=new ue,this.block=new Ht,this.core=new Lt,this.renderer=new ft,this.linkify=new De,this.validateLink=Pe,this.normalizeLink=je,this.normalizeLinkText=Ze,this.utils=lt,this.helpers=U({},ht),this.options={},this.configure(t),e&&this.set(e)}return $e.prototype.set=function(t){return U(this.options,t),this},$e.prototype.configure=function(t){const e=this;if(Z(t)){const e=t;if(!(t=Te[e]))throw new Error('Wrong `markdown-it` preset "'+e+'", check name')}if(!t)throw new Error("Wrong `markdown-it` preset, can't be empty");return t.options&&e.set(t.options),t.components&&Object.keys(t.components).forEach((function(r){t.components[r].rules&&e[r].ruler.enableOnly(t.components[r].rules),t.components[r].rules2&&e[r].ruler2.enableOnly(t.components[r].rules2)})),this},$e.prototype.enable=function(t,e){let r=[];Array.isArray(t)||(t=[t]),["core","block","inline"].forEach((function(e){r=r.concat(this[e].ruler.enable(t,!0))}),this),r=r.concat(this.inline.ruler2.enable(t,!0));const n=t.filter((function(t){return r.indexOf(t)<0}));if(n.length&&!e)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+n);return this},$e.prototype.disable=function(t,e){let r=[];Array.isArray(t)||(t=[t]),["core","block","inline"].forEach((function(e){r=r.concat(this[e].ruler.disable(t,!0))}),this),r=r.concat(this.inline.ruler2.disable(t,!0));const n=t.filter((function(t){return r.indexOf(t)<0}));if(n.length&&!e)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+n);return this},$e.prototype.use=function(t){const e=[this].concat(Array.prototype.slice.call(arguments,1));return t.apply(t,e),this},$e.prototype.parse=function(t,e){if("string"!=typeof t)throw new Error("Input data should be a String");const r=new this.core.State(t,this,e);return this.core.process(r),r.tokens},$e.prototype.render=function(t,e){return e=e||{},this.renderer.render(this.parse(t,e),this.options,e)},$e.prototype.parseInline=function(t,e){const r=new this.core.State(t,this,e);return r.inlineMode=!0,this.core.process(r),r.tokens},$e.prototype.renderInline=function(t,e){return e=e||{},this.renderer.render(this.parseInline(t,e),this.options,e)},$e})); diff --git a/kibot/resources/navigate_results/markdown-it.min.js:Zone.Identifier b/kibot/resources/navigate_results/markdown-it.min.js:Zone.Identifier new file mode 100644 index 000000000..1d77cfe45 --- /dev/null +++ b/kibot/resources/navigate_results/markdown-it.min.js:Zone.Identifier @@ -0,0 +1,3 @@ +[ZoneTransfer] +ZoneId=3 +HostUrl=https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js From 8ce9b8296cf378ad81f4c3490830ddefef72a7dc Mon Sep 17 00:00:00 2001 From: Vincent Nguyen <vincen7.nguyen@gmail.com> Date: Sat, 18 Jan 2025 15:14:17 +0100 Subject: [PATCH 14/14] [Any Navigate Results][Removed] Unused imports --- kibot/out_any_navigate_results.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kibot/out_any_navigate_results.py b/kibot/out_any_navigate_results.py index 9e5da1c45..276e94a7a 100644 --- a/kibot/out_any_navigate_results.py +++ b/kibot/out_any_navigate_results.py @@ -11,13 +11,12 @@ import pprint import re from shutil import copy2 -from math import ceil from .bom.kibot_logo import KIBOT_LOGO, KIBOT_LOGO_W, KIBOT_LOGO_H from .error import KiPlotConfigurationError from .gs import GS from .optionable import Optionable from .out_base import VariantOptions -from .kiplot import config_output, get_output_dir, run_command +from .kiplot import config_output, run_command from .misc import W_NOTYET, W_MISSTOOL, W_NOOUTPUTS, read_png, force_list from .pre_base import BasePreFlight from .registrable import RegOutput