diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..2ea7395 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +filename = + *.py +max-line-length = 120 +extend-exclude = + venv/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a2b762..68b0a60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,26 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} verbose: true + release: + if: ${{ needs.setup_release.outputs.publish_release == 'true' }} + needs: + - setup_release + - build + runs-on: ubuntu-latest + steps: + - name: Create Release + id: action + uses: LizardByte/create-release-action@v2025.102.13208 + with: + allowUpdates: false + artifacts: '' + body: ${{ needs.setup_release.outputs.release_body }} + generateReleaseNotes: ${{ needs.setup_release.outputs.release_generate_release_notes }} + name: ${{ needs.setup_release.outputs.release_tag }} + prerelease: true + tag: ${{ needs.setup_release.outputs.release_tag }} + token: ${{ secrets.GH_BOT_TOKEN }} + publish-gpr: if: ${{ needs.setup_release.outputs.publish_release == 'true' }} needs: diff --git a/.github/workflows/update-docs.yml b/.github/workflows/update-docs.yml new file mode 100644 index 0000000..ba84de8 --- /dev/null +++ b/.github/workflows/update-docs.yml @@ -0,0 +1,96 @@ +--- +# This action is centrally managed in https://github.com//.github/ +# Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in +# the above-mentioned repo. + +# Use the `rtd` repository label to identify repositories that should trigger have this workflow. +# If the project slug is not the repository name, add a repository variable named `READTHEDOCS_SLUG` with the value of +# the ReadTheDocs project slug. + +# Update readthedocs on release events. + +name: Update docs + +on: + release: + types: [created, edited, deleted] + +concurrency: + group: "${{ github.workflow }}-${{ github.event.release.tag_name }}" + cancel-in-progress: true + +jobs: + update-docs: + env: + RTD_SLUG: ${{ vars.READTHEDOCS_SLUG }} + RTD_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} + TAG: ${{ github.event.release.tag_name }} + if: >- + !github.event.release.draft + runs-on: ubuntu-latest + steps: + - name: Get RTD_SLUG + run: | + # if the RTD_SLUG is not set, use the repository name in lowercase + if [ -z "${RTD_SLUG}" ]; then + RTD_SLUG=$(echo "${{ github.event.repository.name }}" | tr '[:upper:]' '[:lower:]') + fi + echo "RTD_SLUG=${RTD_SLUG}" >> $GITHUB_ENV + + - name: Deactivate deleted release + if: >- + github.event_name == 'release' && + github.event.action == 'deleted' + run: | + json_body=$(jq -n \ + --arg active "false" \ + --arg hidden "false" \ + --arg privacy_level "public" \ + '{active: $active, hidden: $hidden, privacy_level: $privacy_level}') + + curl \ + -X PATCH \ + -H "Authorization: Token ${RTD_TOKEN}" \ + https://readthedocs.org/api/v3/projects/${RTD_SLUG}/versions/${TAG}/ \ + -H "Content-Type: application/json" \ + -d "$json_body" + + - name: Check if edited release is latest GitHub release + id: check + if: >- + github.event_name == 'release' && + github.event.action == 'edited' + uses: actions/github-script@v7 + with: + script: | + const latestRelease = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + core.setOutput('isLatestRelease', latestRelease.data.tag_name === context.payload.release.tag_name); + + - name: Update RTD project + # changing the default branch in readthedocs makes "latest" point to that branch/tag + # we can also update other properties like description, etc. + if: >- + steps.check.outputs.isLatestRelease == 'true' + run: | + json_body=$(jq -n \ + --arg default_branch "${TAG}" \ + --arg description "${{ github.event.repository.description }}" \ + '{default_branch: $default_branch}') + + # change the default branch to the latest release + curl \ + -X PATCH \ + -H "Authorization: Token ${RTD_TOKEN}" \ + -H "Content-Type: application/json" \ + https://readthedocs.org/api/v3/projects/${RTD_SLUG}/ \ + -d "$json_body" + + # trigger a build for the latest version + curl \ + -X POST \ + -H "Authorization: Token ${RTD_TOKEN}" \ + https://readthedocs.org/api/v3/projects/${RTD_SLUG}/versions/latest/builds/ diff --git a/.gitignore b/.gitignore index a00257a..42f2718 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,7 @@ junit.xml coverage/ # Ignore build artifacts +_readthedocs/ +build/ dist/ lizardbyte-shared-web*.tgz diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..da79057 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,31 @@ +--- +version: 2 +build: + os: "ubuntu-24.04" + tools: + nodejs: "22" + python: "3.13" + ruby: "3.3" + commands: + - 'echo "output directory: ${READTHEDOCS_OUTPUT}html"' + # shared-web build + - npm install + - npm run build + - npm pack + - mkdir -p ${READTHEDOCS_OUTPUT}html/dist + - cp -r dist/* ${READTHEDOCS_OUTPUT}html/dist + # jsdoc build + - npm run generate-docs + # jekyll example build + - 'echo "baseurl: projects/shared-web/$READTHEDOCS_VERSION"/jekyll >> ./examples/jekyll/_config.yml' + - cd examples/jekyll && npm install + - cd examples/jekyll && npm run build + # doxygen example build + - cd examples/doxygen && npm install + - cd examples/doxygen && npm run build + # sphinx example build + - cd examples/sphinx && npm install + - cd examples/sphinx && npm run build + - cd examples/sphinx && npm run lint + # debug output + - cd ${READTHEDOCS_OUTPUT}html && ls -la -R diff --git a/docs/static/avatar.png b/docs/static/avatar.png new file mode 100644 index 0000000..e131ef4 Binary files /dev/null and b/docs/static/avatar.png differ diff --git a/docs/static/favicon.ico b/docs/static/favicon.ico new file mode 100644 index 0000000..79620bf Binary files /dev/null and b/docs/static/favicon.ico differ diff --git a/docs/static/logo.png b/docs/static/logo.png new file mode 100644 index 0000000..d82d3f0 Binary files /dev/null and b/docs/static/logo.png differ diff --git a/examples/doxygen/Doxyfile b/examples/doxygen/Doxyfile new file mode 100644 index 0000000..75ad772 --- /dev/null +++ b/examples/doxygen/Doxyfile @@ -0,0 +1,55 @@ +DOXYFILE_ENCODING = UTF-8 + +# project metadata +DOCSET_BUNDLE_ID = dev.lizardbyte.shared-web.doxygen +DOCSET_PUBLISHER_ID = dev.lizardbyte.shared-web.doxygen +DOCSET_PUBLISHER_NAME = LizardByte +PROJECT_BRIEF = "Example use of @lizardbyte/shared-web in doxygen html." +PROJECT_ICON = $(READTHEDOCS_OUTPUT)html/favicon.ico +PROJECT_LOGO = $(READTHEDOCS_OUTPUT)html/logo.png +PROJECT_NAME = shared-web Doxygen sample + +# doxygen-awesome-css +HTML_COLORSTYLE = LIGHT # required with Doxygen >= 1.9.5 +HTML_COLORSTYLE_HUE = 209 +HTML_COLORSTYLE_SAT = 255 +HTML_COLORSTYLE_GAMMA = 113 +HTML_COPY_CLIPBOARD = NO # required for Doxygen >= 1.10.0 +HTML_EXTRA_FILES = ./node_modules/@jothepro/doxygen-awesome-css/doxygen-awesome-darkmode-toggle.js +HTML_EXTRA_FILES += ./node_modules/@jothepro/doxygen-awesome-css/doxygen-awesome-fragment-copy-button.js +HTML_EXTRA_FILES += ./node_modules/@jothepro/doxygen-awesome-css/doxygen-awesome-paragraph-link.js +HTML_EXTRA_FILES += ./node_modules/@jothepro/doxygen-awesome-css/doxygen-awesome-interactive-toc.js +HTML_EXTRA_FILES += ./node_modules/@jothepro/doxygen-awesome-css/doxygen-awesome-tabs.js +HTML_EXTRA_STYLESHEET = ./node_modules/@jothepro/doxygen-awesome-css/doxygen-awesome.css +HTML_HEADER = header.html + +# @lizardbyte/shared-web +HTML_EXTRA_FILES += ./node_modules/@lizardbyte/shared-web/dist/crowdin.js +HTML_EXTRA_STYLESHEET += ./node_modules/@lizardbyte/shared-web/dist/crowdin-doxygen-css.css + +# general settings +CASE_SENSE_NAMES = YES +CREATE_SUBDIRS = NO +DISABLE_INDEX = NO +FULL_SIDEBAR = NO +GENERATE_HTML = YES +GENERATE_LATEX = NO +GENERATE_TREEVIEW = YES +GENERATE_XML = NO +HAVE_DOT = NO +HTML_OUTPUT = html +MARKDOWN_ID_STYLE = GITHUB +MARKDOWN_SUPPORT = YES +PROJECT_NUMBER = $(READTHEDOCS_VERSION) +OUTPUT_DIRECTORY = $(READTHEDOCS_OUTPUT)html/doxygen +RECURSIVE = YES +WARN_AS_ERROR = FAIL_ON_WARNINGS +WARN_IF_DOC_ERROR = YES +WARN_IF_INCOMPLETE_DOC = YES +WARN_IF_UNDOC_ENUM_VAL = YES +WARN_IF_UNDOCUMENTED = YES +WARN_NO_PARAMDOC = YES +WARNINGS = YES + +USE_MDFILE_AS_MAINPAGE = sample.md +INPUT = sample.md diff --git a/examples/doxygen/header.html b/examples/doxygen/header.html new file mode 100644 index 0000000..c17326e --- /dev/null +++ b/examples/doxygen/header.html @@ -0,0 +1,96 @@ + + + + + + + + +$projectname: $title +$title + + + + + + + + + +$treeview +$search +$mathjax +$darkmode + +$extrastylesheet + + + + + + + + + + + + + + + + + +
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname $projectnumber +
+
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + diff --git a/examples/doxygen/package.json b/examples/doxygen/package.json new file mode 100644 index 0000000..0786d88 --- /dev/null +++ b/examples/doxygen/package.json @@ -0,0 +1,12 @@ +{ + "name": "shared-web-doxygen-example", + "version": "0.0.0", + "description": "Example use of @lizardbyte/shared-web in doxygen html.", + "dependencies": { + "@jothepro/doxygen-awesome-css": "github:jothepro/doxygen-awesome-css#v2.3.4", + "@lizardbyte/shared-web": "file:../../lizardbyte-shared-web-0.0.0.tgz" + }, + "scripts": { + "build": "doxygen Doxyfile" + } +} diff --git a/examples/doxygen/sample.md b/examples/doxygen/sample.md new file mode 100644 index 0000000..5bfbb69 --- /dev/null +++ b/examples/doxygen/sample.md @@ -0,0 +1,33 @@ +# shared-web Doxygen sample + +This is a sample project for Doxygen with doxygen-awesome-css theme. This project allows you to visualize how the +widgets appear in Doxygen documentation. + +This will be automatically enabled in our [doxyconfig](https://github.com/LizardByte/doxyconfig) repo. + +## Widgets + +You can include widgets in your Doxygen documentation by adding the following to your +Doxyfile (or Doxygen configuration file). You may need to adjust the paths depending on your project structure. + +### CrowdIn + +Doxyfile: +```doxygen +HTML_EXTRA_FILES += ../node_modules/@lizardbyte/shared-web/dist/crowdin.js +HTML_EXTRA_STYLESHEET += ../node_modules/@lizardbyte/shared-web/dist/crowdin-doxygen-css.css +``` + +header.html: +```html + + + +``` + +
+ + [TOC] +
diff --git a/examples/jekyll/Gemfile b/examples/jekyll/Gemfile new file mode 100644 index 0000000..be173b2 --- /dev/null +++ b/examples/jekyll/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec diff --git a/examples/jekyll/_config.yml b/examples/jekyll/_config.yml new file mode 100644 index 0000000..b7017ec --- /dev/null +++ b/examples/jekyll/_config.yml @@ -0,0 +1,51 @@ +--- +# See https://github.com/daattali/beautiful-jekyll/blob/master/_config.yml for documented options + +remote_theme: ReenigneArcher/beautiful-jekyll@c303cbc58172bba5b1295cff2e6930bdad98a997 +title: shared-web Jekyll sample +author: LizardByte +avatar: "../avatar.png" +round-avatar: true +title-on-all-pages: true +post_search: false +edit_page_button: true +navbar-var-length: true +keywords: "shared-web, LizardByte, Jekyll, example" +footer-col: "#242526" +footer-hover-col: "#28a9e6" +footer-link-col: "#dfdfdf" +footer-text-col: "#a8aaa8" +hover-col: "#28a9e6" +link-col: "#28a9e6" +navbar-border-col: "#393a3b" +navbar-col: "#151515" +navbar-text-col: "#e4e6eb" +page-col: "#303436" +text-col: "#e4e4e4" +mobile-theme-col: "#05FF3B" +site-css: + - "../dist/crowdin-bootstrap-css.css" +site-js: + - "../dist/crowdin.js" + - "../dist/discord.js" + - "/assets/js/crowdin-init.js" + - "/assets/js/discord-init.js" + +# Advanced settings +timezone: "America/New_York" +markdown: kramdown +highlighter: rouge +permalink: /:year-:month-:day-:title/ +paginate: 5 +kramdown: + input: GFM + +# Exclude these files from production site +exclude: + - Gemfile + - Gemfile.lock + +plugins: + - jekyll-paginate + - jekyll-remote-theme + - jekyll-sitemap diff --git a/examples/jekyll/assets/js/crowdin-init.js b/examples/jekyll/assets/js/crowdin-init.js new file mode 100644 index 0000000..9ae9050 --- /dev/null +++ b/examples/jekyll/assets/js/crowdin-init.js @@ -0,0 +1 @@ +window.initCrowdIn('LizardByte-docs', null); diff --git a/examples/jekyll/assets/js/discord-init.js b/examples/jekyll/assets/js/discord-init.js new file mode 100644 index 0000000..e9e9585 --- /dev/null +++ b/examples/jekyll/assets/js/discord-init.js @@ -0,0 +1 @@ +window.initDiscord(); diff --git a/examples/jekyll/favicon.ico b/examples/jekyll/favicon.ico new file mode 100644 index 0000000..79620bf Binary files /dev/null and b/examples/jekyll/favicon.ico differ diff --git a/examples/jekyll/index.md b/examples/jekyll/index.md new file mode 100644 index 0000000..003c96d --- /dev/null +++ b/examples/jekyll/index.md @@ -0,0 +1,10 @@ +--- +layout: "page" +title: "shared-web Jekyll sample" +--- + +This is a sample project for Jekyll with Beautiful-Jekyll theme. This project allows you to visualize how the +widgets appear in a Jekyll site. + +This will be automatically enabled in our +[LizardByte.github.io](https://github.com/LizardByte/LizardByte.github.io) repo. diff --git a/examples/jekyll/package.json b/examples/jekyll/package.json new file mode 100644 index 0000000..6342269 --- /dev/null +++ b/examples/jekyll/package.json @@ -0,0 +1,17 @@ +{ + "name": "shared-web-jekyll-example", + "version": "0.0.0", + "description": "Example use of @lizardbyte/shared-web in Jekyll.", + "dependencies": { + "@lizardbyte/shared-web": "file:../../lizardbyte-shared-web-0.0.0.tgz" + }, + "devDependencies": { + "npm-run-all": "4.1.5" + }, + "scripts": { + "postinstall": "npm-run-all postinstall:*", + "postinstall:bundler": "echo 'Installing bundler...' && gem install bundler", + "postinstall:bundle": "echo 'Installing Jekyll dependencies...' && bundle install", + "build": "jekyll build --verbose --destination ${READTHEDOCS_OUTPUT:-build/}html/jekyll" + } +} diff --git a/examples/jekyll/shared-web-jekyll.gemspec b/examples/jekyll/shared-web-jekyll.gemspec new file mode 100644 index 0000000..b88b9fc --- /dev/null +++ b/examples/jekyll/shared-web-jekyll.gemspec @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +Gem::Specification.new do |spec| + spec.name = "shared-web-jekyll" + spec.version = "0.0.0" + spec.authors = ["ReenigneArcher"] + + spec.summary = "Example of using shared-web in a Jekyll site." + + spec.files = `git ls-files -z`.split("\x0").select { |f| f.match(%r{^(assets|_layouts|_includes|_data)}i) } + + spec.add_runtime_dependency "jekyll", ">= 3.9.3" + spec.add_runtime_dependency "jekyll-paginate", "~> 1.1" + spec.add_runtime_dependency "jekyll-sitemap", "~> 1.4" + spec.add_runtime_dependency "kramdown-parser-gfm", "~> 1.1" + spec.add_runtime_dependency "kramdown", "~> 2.3" + spec.add_runtime_dependency "webrick", "~> 1.8" + + spec.add_development_dependency "jekyll-remote-theme", "~> 0.4" +end diff --git a/examples/sphinx/.rstcheck.cfg b/examples/sphinx/.rstcheck.cfg new file mode 100644 index 0000000..fb16fa6 --- /dev/null +++ b/examples/sphinx/.rstcheck.cfg @@ -0,0 +1,8 @@ +# configuration file for rstcheck, an rst linting tool +# https://rstcheck.readthedocs.io/en/latest/usage/config + +[rstcheck] +ignore_directives = + include, + mdinclude, + todo, diff --git a/examples/sphinx/Makefile b/examples/sphinx/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/examples/sphinx/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/examples/sphinx/make.bat b/examples/sphinx/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/examples/sphinx/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/examples/sphinx/package.json b/examples/sphinx/package.json new file mode 100644 index 0000000..7822586 --- /dev/null +++ b/examples/sphinx/package.json @@ -0,0 +1,13 @@ +{ + "name": "shared-web-sphinx-example", + "version": "0.0.0", + "description": "Example use of @lizardbyte/shared-web in Sphinx.", + "dependencies": { + "@lizardbyte/shared-web": "file:../../lizardbyte-shared-web-0.0.0.tgz" + }, + "scripts": { + "postinstall": "echo 'Installing Python dependencies...' && python -m pip install -r requirements.txt --no-cache-dir", + "build": "python -m sphinx -b html source ${READTHEDOCS_OUTPUT:-build/}html/sphinx/html", + "lint": "python -m rstcheck -r ." + } +} diff --git a/examples/sphinx/requirements.txt b/examples/sphinx/requirements.txt new file mode 100644 index 0000000..1cc61d5 --- /dev/null +++ b/examples/sphinx/requirements.txt @@ -0,0 +1,3 @@ +furo==2024.8.6 +rstcheck[sphinx]==6.2.4 +Sphinx==8.2.3 diff --git a/examples/sphinx/source/_static/js/crowdin.js b/examples/sphinx/source/_static/js/crowdin.js new file mode 100644 index 0000000..c801b66 --- /dev/null +++ b/examples/sphinx/source/_static/js/crowdin.js @@ -0,0 +1 @@ +window.initCrowdIn('LizardByte-docs', 'sphinx') diff --git a/examples/sphinx/source/conf.py b/examples/sphinx/source/conf.py new file mode 100644 index 0000000..5aeaf0d --- /dev/null +++ b/examples/sphinx/source/conf.py @@ -0,0 +1,94 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# standard imports +from datetime import datetime +import os + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + +script_dir = os.path.dirname(os.path.abspath(__file__)) # the directory of this file +source_dir = os.path.dirname(script_dir) # the source folder directory +root_dir = os.path.dirname(os.path.dirname(source_dir)) # the root folder directory + +# -- Project information ----------------------------------------------------- +project = 'shared-web Sphinx sample' +project_copyright = f'{datetime.now().year}, {project}' +author = 'ReenigneArcher' + +# The full version, including alpha/beta/rc tags +version = os.getenv('READTHEDOCS_VERSION', 'latest') + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autosectionlabel', + 'sphinx.ext.todo', # enable to-do sections + 'sphinx.ext.viewcode', # add links to view source code +] + +# Add any paths that contain templates here, relative to this directory. +# templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# Extensions to include. +source_suffix = { + '.rst': 'restructuredtext', +} + + +# -- Options for HTML output ------------------------------------------------- + +# images +html_favicon = os.path.join(root_dir, 'docs', 'static', 'favicon.ico') +html_logo = os.path.join(root_dir, 'docs', 'static', 'logo.png') + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# These paths are either relative to html_static_path +# or fully qualified paths (eg. https://...) +html_css_files = [ + # use jsdelivr for an easy way to include the css + # 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@latest/dist/crowdin-furo-css.css', + '../../../dist/crowdin-furo-css.css', # crowdin style from the readthedocs build +] +html_js_files = [ + # use jsdelivr for an easy way to include the script + # 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@latest/dist/crowdin.js', + '../../../dist/crowdin.js', # crowdin language selector from the readthedocs build + 'js/crowdin.js', # initialize crowdin language selector +] + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'furo' + +html_theme_options = { + "top_of_page_button": "edit", + "source_edit_link": "https://github.com/lizardbyte/shared-web/blob/master/examples/sphinx/source/{filename}", +} + +# extension config options +autosectionlabel_prefix_document = True # Make sure the target is unique +todo_include_todos = True + +# disable epub mimetype warnings +# https://github.com/readthedocs/readthedocs.org/blob/eadf6ac6dc6abc760a91e1cb147cc3c5f37d1ea8/docs/conf.py#L235-L236 +suppress_warnings = ["epub.unknown_project_files"] diff --git a/examples/sphinx/source/index.rst b/examples/sphinx/source/index.rst new file mode 100644 index 0000000..8e47d94 --- /dev/null +++ b/examples/sphinx/source/index.rst @@ -0,0 +1,29 @@ +shared-web Sphinx sample +======================== + +This is a sample project for Sphinx with Furo theme. This project allows you to visualize how the +widgets appear in Sphinx documentation. + +Widgets +------- + +CrowdIn +~~~~~~~ + +Add the following to the Sphinx configuration file to enable the CrowdIn language selector: + +.. code-block:: python + + html_css_files = [ + 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@latest/dist/crowdin-furo-css.css', + ] + html_js_files = [ + 'https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@latest/dist/crowdin.js', + 'js/crowdin.js', # initialize crowdin language selector + ] + +Then create a file named ``js/crowdin.js`` located in the ``html_static_path`` directory, with the following content: + +.. code-block:: javascript + + window.initCrowdIn('LizardByte-docs', 'sphinx') diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..06e5364 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,68 @@ +{ + "source": { + "include": ["src", "README.md"], + "includePattern": ".js$", + "excludePattern": "(node_modules/|docs/build)" + }, + "plugins": ["plugins/markdown"], + "opts": { + "encoding": "utf8", + "readme": "./README.md", + "destination": "./_readthedocs/html", + "recurse": true, + "verbose": true, + "template": "node_modules/clean-jsdoc-theme", + "theme_opts": { + "add_scripts": "window.initCrowdIn('LizardByte-docs', null);", + "default_theme": "fallback-dark", + "favicon": "favicon.ico", + "include_css": ["./dist/crowdin-clean-jsdoc-css.css"], + "include_js": ["./dist/crowdin.js"], + "menu": [ + { + "title": "Jekyll Sample", + "link": "jekyll", + "target": "_blank", + "class": "", + "id": "jekyll-sample" + }, + { + "title": "Doxygen Sample", + "link": "doxygen/html", + "target": "_blank", + "class": "", + "id": "doxygen-sample" + }, + { + "title": "Sphinx Sample", + "link": "sphinx/html", + "target": "_blank", + "class": "", + "id": "sphinx-sample" + }, + { + "title": "❤ Donate", + "link": "https://app.lizardbyte.dev", + "target": "_blank", + "class": "", + "id": "donate" + } + ] + } + }, + "markdown": { + "hardwrap": false, + "idInHeadings": true + }, + "templates": { + "cleverLinks": true, + "monospaceLinks": false, + "default": { + "staticFiles": { + "include": [ + "./docs/static" + ] + } + } + } +} diff --git a/package.json b/package.json index 33d3da4..a51ef7a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@eslint/js": "9.22.0", "@jest/globals": "29.7.0", "babel-loader": "10.0.0", + "clean-jsdoc-theme": "4.3.0", "cross-env": "7.0.3", "css-loader": "7.1.2", "eslint": "9.22.0", @@ -38,6 +39,7 @@ "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-junit": "16.0.0", + "jsdoc": "4.0.4", "mini-css-extract-plugin": "2.9.2", "node-fetch": "3.3.2", "npm-run-all": "4.1.5", @@ -58,6 +60,7 @@ }, "scripts": { "build": "cross-env NODE_ENV=production webpack", + "generate-docs": "jsdoc --configure jsdoc.json --verbose", "start": "webpack serve", "test": "npm-run-all test:unit test:report test:lint", "test:unit": "jest --coverage", diff --git a/src/css/crowdin-bootstrap.scss b/src/css/crowdin-bootstrap.scss new file mode 100644 index 0000000..a2f1cc5 --- /dev/null +++ b/src/css/crowdin-bootstrap.scss @@ -0,0 +1,20 @@ +#crowdin-language-picker .cr-picker-button, +#crowdin-language-picker .cr-picker-submenu { + background-color: var(--navbar-col) !important; + border: 1px solid var(--navbar-border-col) !important; + color: var(--navbar-text-col) !important; +} + +#crowdin-language-picker .cr-picker-button:hover, +#crowdin-language-picker .cr-picker-submenu > a:hover { + background-color: var(--navbar-text-col) !important; + color: var(--navbar-col) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a { + color: var(--navbar-text-col) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a.cr-selected { + color: var(--navbar-border-col) !important; +} diff --git a/src/css/crowdin-clean-jsdoc.scss b/src/css/crowdin-clean-jsdoc.scss new file mode 100644 index 0000000..c7056b7 --- /dev/null +++ b/src/css/crowdin-clean-jsdoc.scss @@ -0,0 +1,36 @@ +:root { + --crowdin-primary-color: #f7f7f7; + --crowdin-secondary-color: #565656; + --crowdin-primary-text-color: #111111; + --crowdin-secondary-text-color: #c5c5c9; +} + +@media (prefers-color-scheme: dark) { + body[data-theme="dark"] { + --crowdin-primary-color: #222222; + --crowdin-secondary-color: #717171; + --crowdin-primary-text-color: #ffffff; + --crowdin-secondary-text-color: #8b949e; + } +} + +#crowdin-language-picker .cr-picker-button, +#crowdin-language-picker .cr-picker-submenu { + background-color: var(--crowdin-primary-color) !important; + border: 1px solid var(--crowdin-secondary-color) !important; + color: var(--crowdin-primary-text-color) !important; +} + +#crowdin-language-picker .cr-picker-button:hover, +#crowdin-language-picker .cr-picker-submenu > a:hover { + background-color: var(--crowdin-primary-text-color) !important; + color: var(--crowdin-primary-color) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a { + color: var(--crowdin-primary-text-color) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a.cr-selected { + color: var(--crowdin-secondary-text-color) !important; +} diff --git a/src/css/crowdin-doxygen.scss b/src/css/crowdin-doxygen.scss new file mode 100644 index 0000000..7670c09 --- /dev/null +++ b/src/css/crowdin-doxygen.scss @@ -0,0 +1,20 @@ +#crowdin-language-picker .cr-picker-button, +#crowdin-language-picker .cr-picker-submenu { + background-color: var(--page-background-color) !important; + border: 1px solid var(--page-secondary-foreground-color) !important; + color: var(--page-foreground-color) !important; +} + +#crowdin-language-picker .cr-picker-button:hover, +#crowdin-language-picker .cr-picker-submenu > a:hover { + background-color: var(--page-foreground-color) !important; + color: var(--page-background-color) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a { + color: var(--page-foreground-color) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a.cr-selected { + color: var(--page-secondary-foreground-color) !important; +} diff --git a/src/css/crowdin-furo.scss b/src/css/crowdin-furo.scss new file mode 100644 index 0000000..eda4616 --- /dev/null +++ b/src/css/crowdin-furo.scss @@ -0,0 +1,20 @@ +#crowdin-language-picker .cr-picker-button, +#crowdin-language-picker .cr-picker-submenu { + background-color: var(--color-code-background) !important; + border: 1px solid var(--color-foreground-border) !important; + color: var(--color-code-foreground) !important; +} + +#crowdin-language-picker .cr-picker-button:hover, +#crowdin-language-picker .cr-picker-submenu > a:hover { + background-color: var(--color-code-foreground) !important; + color: var(--color-code-background) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a { + color: var(--color-code-foreground) !important; +} + +#crowdin-language-picker .cr-picker-submenu > a.cr-selected { + color: var(--color-background-item) !important; +} diff --git a/src/js/crowdin-bootstrap-css.js b/src/js/crowdin-bootstrap-css.js new file mode 100644 index 0000000..ca2d617 --- /dev/null +++ b/src/js/crowdin-bootstrap-css.js @@ -0,0 +1 @@ +import "../css/crowdin-bootstrap.scss"; diff --git a/src/js/crowdin-clean-jsdoc-css.js b/src/js/crowdin-clean-jsdoc-css.js new file mode 100644 index 0000000..7a8f5e2 --- /dev/null +++ b/src/js/crowdin-clean-jsdoc-css.js @@ -0,0 +1 @@ +import "../css/crowdin-clean-jsdoc.scss"; diff --git a/src/js/crowdin-doxygen-css.js b/src/js/crowdin-doxygen-css.js new file mode 100644 index 0000000..735e3ed --- /dev/null +++ b/src/js/crowdin-doxygen-css.js @@ -0,0 +1 @@ +import "../css/crowdin-doxygen.scss"; diff --git a/src/js/crowdin-furo-css.js b/src/js/crowdin-furo-css.js new file mode 100644 index 0000000..2b53616 --- /dev/null +++ b/src/js/crowdin-furo-css.js @@ -0,0 +1 @@ +import "../css/crowdin-furo.scss"; diff --git a/src/js/crowdin.js b/src/js/crowdin.js index d330d0f..dbbc7ae 100644 --- a/src/js/crowdin.js +++ b/src/js/crowdin.js @@ -2,17 +2,17 @@ const loadScript = require('./load-script'); /** * Initializes Crowdin translation widget based on project and UI platform. - * @param {string} project - Project name ('LizardByte' or 'LizardByte-docs') - * @param {string|null} platform - UI platform ('bootstrap', 'sphinx', or null) + * @param {string} project - Project name ('LizardByte' or 'LizardByte-docs'). + * @param {string|null} platform - UI platform ('sphinx', or null). */ -function initCrowdIn(project = 'LizardByte', platform = 'bootstrap') { +function initCrowdIn(project = 'LizardByte', platform = null) { // Input validation if (!['LizardByte', 'LizardByte-docs'].includes(project)) { console.error('Invalid project. Must be "LizardByte" or "LizardByte-docs"'); return; } - if (!['bootstrap', 'sphinx', null].includes(platform)) { - console.error('Invalid UI. Must be "bootstrap", "sphinx", or null'); + if (!['sphinx', null].includes(platform)) { + console.error('Invalid UI. Must be "sphinx", or null'); return; } @@ -79,30 +79,23 @@ function initCrowdIn(project = 'LizardByte', platform = 'bootstrap') { if (platform === null) { return; } - setTimeout(() => { - const container = document.getElementById('crowdin-language-picker'); - const button = document.getElementsByClassName('cr-picker-button')[0]; - const menu = document.getElementsByClassName('cr-picker-submenu')[0]; - const selected = document.getElementsByClassName('cr-selected')[0]; - if (platform === 'bootstrap') { - button.classList.add('border-white', 'btn', 'btn-outline-light', 'bg-dark', 'text-white', 'rounded-0'); - menu.classList.add('border-white', 'bg-dark', 'text-white', 'rounded-0'); - selected.classList.add('text-white'); - } else if (platform === 'sphinx') { - container.classList.remove('cr-position-bottom-left') - container.style.width = button.offsetWidth + 10 + 'px'; - container.style.position = 'relative'; - container.style.left = '10px'; - container.style.bottom = '10px'; + const container = document.getElementById('crowdin-language-picker'); + const button = document.getElementsByClassName('cr-picker-button')[0]; - // get rst versions - const sidebar = document.getElementsByClassName('sidebar-sticky')[0]; + if (platform === 'sphinx') { + container.classList.remove('cr-position-bottom-left') + container.style.width = button.offsetWidth + 10 + 'px'; + container.style.position = 'relative'; + container.style.left = '10px'; + container.style.bottom = '10px'; - // move button to related pages - sidebar.appendChild(container); - } - }, 500); // Short delay to ensure elements are available + // get rst versions + const sidebar = document.getElementsByClassName('sidebar-sticky')[0]; + + // move button to related pages + sidebar.appendChild(container); + } }); } diff --git a/src/js/discord.js b/src/js/discord.js index 43907ea..ef25380 100644 --- a/src/js/discord.js +++ b/src/js/discord.js @@ -5,11 +5,19 @@ const loadScript = require('./load-script'); const { fetchRandomQuote } = require('./random-quote'); +/** + * Notify the user with a random quote. + * @param {Object} quote - The quote object. + * @param {Crate} crate - The Crate object. + */ function randomQuote(quote, crate) { let the_quote = quote['quote_safe'] || quote['quote']; crate.notify(the_quote); } +/** + * Initialize the Discord widget. + */ function initDiscord() { loadScript('https://cdn.jsdelivr.net/npm/@widgetbot/crate@3', function() { let widgetbot = new Crate({ diff --git a/src/js/format-number.js b/src/js/format-number.js index f31a1ed..3dcf21c 100644 --- a/src/js/format-number.js +++ b/src/js/format-number.js @@ -1,8 +1,8 @@ /** - * Format a number to a human-readable string - * @param num The number to format - * @param decimalPlaces The number of decimal places to include in the formatted string - * @returns {string} + * Format a number to a human-readable string. + * @param {number} num The number to format. + * @param {number} decimalPlaces The number of decimal places to include in the formatted string. + * @returns {string} The formatted number as a string with suffix (k, M, etc.). */ function formatNumber(num, decimalPlaces = 1) { if (num >= 1000000) { diff --git a/src/js/levenshtein-distance.js b/src/js/levenshtein-distance.js index d7ea625..9c367fb 100644 --- a/src/js/levenshtein-distance.js +++ b/src/js/levenshtein-distance.js @@ -1,3 +1,9 @@ +/** + * Calculate the levenshtein distance between two strings. + * @param {string} a - The first string to compare. + * @param {string} b - The second string to compare. + * @returns {number|*} - The percentage of the levenshtein distance. + */ function levenshteinDistance(a, b) { if (a.length === 0) return b.length; if (b.length === 0) return a.length; diff --git a/src/js/load-script.js b/src/js/load-script.js index 26fe180..dddfcf4 100644 --- a/src/js/load-script.js +++ b/src/js/load-script.js @@ -1,7 +1,7 @@ /** * Load a script asynchronously, and add it to the DOM. Optionally, call a callback when the script has loaded. - * @param url The URL of the script to load. - * @param callback An optional callback to call when the script has loaded. + * @param {string} url The URL of the script to load. + * @param {Function} callback An optional callback to call when the script has loaded. */ function loadScript(url, callback) { const script = document.createElement('script'); diff --git a/src/js/ranking-sorter.js b/src/js/ranking-sorter.js index 9725f94..0960e8b 100644 --- a/src/js/ranking-sorter.js +++ b/src/js/ranking-sorter.js @@ -1,8 +1,8 @@ /** * Sorts an array of objects by two keys in descending order. - * @param firstKey The primary key to sort by. - * @param secondKey The secondary key to sort by, in case the first key is equal. - * @returns {(function(*, *): (number))|*} The sorting function. + * @param {string} firstKey The primary key to sort by. + * @param {string} secondKey The secondary key to sort by, in case the first key is equal. + * @returns {Function} The sorting function that returns a number. */ function rankingSorter(firstKey, secondKey) { return function(a, b) { diff --git a/src/js/sleep.js b/src/js/sleep.js index c7fedca..77d4ccb 100644 --- a/src/js/sleep.js +++ b/src/js/sleep.js @@ -1,7 +1,7 @@ /** * Sleep for a given amount of time. - * @param ms The time to sleep in milliseconds - * @returns {Promise} A promise that resolves after the given time + * @param {number} ms The time to sleep in milliseconds. + * @returns {Promise} A promise that resolves after the given time. */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); diff --git a/tests/crowdin.test.js b/tests/crowdin.test.js index fb47b7f..b61c44d 100644 --- a/tests/crowdin.test.js +++ b/tests/crowdin.test.js @@ -56,7 +56,7 @@ describe('initCrowdIn', () => { it('should validate platform parameter', () => { initCrowdIn('LizardByte', 'invalidPlatform'); - expect(console.error).toHaveBeenCalledWith('Invalid UI. Must be "bootstrap", "sphinx", or null'); + expect(console.error).toHaveBeenCalledWith('Invalid UI. Must be "sphinx", or null'); }); it('should initialize proxyTranslator with LizardByte settings', () => { @@ -100,22 +100,6 @@ describe('initCrowdIn', () => { expect(button.classList.contains('btn')).toBe(false); }); - it('should apply bootstrap styling', () => { - initCrowdIn('LizardByte', 'bootstrap'); - - // Simulate script loading and UI styling timeout - jest.runAllTimers(); - - const button = document.getElementsByClassName('cr-picker-button')[0]; - const menu = document.getElementsByClassName('cr-picker-submenu')[0]; - const selected = document.getElementsByClassName('cr-selected')[0]; - - expect(button.classList.contains('btn')).toBe(true); - expect(button.classList.contains('btn-outline-light')).toBe(true); - expect(menu.classList.contains('bg-dark')).toBe(true); - expect(selected.classList.contains('text-white')).toBe(true); - }); - it('should apply sphinx styling', () => { initCrowdIn('LizardByte', 'sphinx'); diff --git a/webpack.config.js b/webpack.config.js index a64acc5..e063322 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -7,6 +7,10 @@ let production = process.env.NODE_ENV === 'production'; let config = { entry: { 'crowdin': './src/js/crowdin', + 'crowdin-bootstrap-css': './src/js/crowdin-bootstrap-css', + 'crowdin-clean-jsdoc-css': './src/js/crowdin-clean-jsdoc-css', + 'crowdin-doxygen-css': './src/js/crowdin-doxygen-css', + 'crowdin-furo-css': './src/js/crowdin-furo-css', 'discord': './src/js/discord', 'format-number': './src/js/format-number', 'levenshtein-distance': './src/js/levenshtein-distance',