diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..645e420 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,32 @@ + +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.231.6/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster +ARG VARIANT="3.10-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# Poetry +ARG POETRY_VERSION="none" +RUN if [ "${POETRY_VERSION}" != "none" ]; then su vscode -c "umask 0002 && pip3 install poetry==${POETRY_VERSION}"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 + +# Update locale settings +RUN apt-get update && \ + export DEBIAN_FRONTEND=noninteractive && \ + apt-get install -y \ + locales && \ + rm -r /var/lib/apt/lists/* + +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..671212e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,99 @@ +{ + "name": "obsidian-metadata", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local on arm64/Apple Silicon. + "VARIANT": "3.10-bullseye", + "POETRY_VERSION": "1.2.2" + } + }, + // Set *default* container specific settings.json values on container create. + "settings": { + "autoDocstring.startOnNewLine": true, + "coverage-gutters.coverageFileNames": ["reports/coverage.xml"], + "coverage-gutters.showGutterCoverage": false, + "coverage-gutters.showLineCoverage": true, + "coverage-gutters.showRulerCoverage": true, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + }, + "editor.rulers": [100], + "python.analysis.completeFunctionParens": true, + "python.formatting.provider": "black", + "python.linting.enabled": true, + "python.linting.mypyEnabled": true, + "python.linting.mypyPath": "mypy", + "python.linting.pylintEnabled": false, + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": false, + "python.testing.pytestEnabled": true, + "python.linting.mypyArgs": [ + "--config-file", + "pyproject.toml", + "--exclude", + "'tests/'" + ], + "python.linting.ignorePatterns": [ + ".vscode/**/*.py", + ".venv/**/*.py" + ], + "python.venvFolders": ["/home/vscode/.cache/pypoetry/virtualenvs"], + "ruff.importStrategy": "fromEnvironment", + "shellformat.path": "/home/vscode/.local/bin/shfmt", + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/usr/bin/zsh" + } + } + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "bierner.markdown-preview-github-styles", + "charliermarsh.ruff", + "donjayamanne.githistory", + "eamodio.gitlens", + "fcrespo82.markdown-table-formatter", + "foxundermoon.shell-format", + "GitHub.copilot", + "Gruntfuggly.todo-tree", + "mhutchie.git-graph", + "njpwerner.autodocstring", + "oderwat.indent-rainbow", + "redhat.vscode-yaml", + "ryanluker.vscode-coverage-gutters", + "samuelcolvin.jinjahtml", + "shardulm94.trailing-spaces", + "streetsidesoftware.code-spell-checker", + "tamasfe.even-better-toml", + "timonwong.shellcheck", + "Tyriar.sort-lines", + "visualstudioexptteam.vscodeintellicode", + "Chouzz.vscode-better-align", + "yzhang.markdown-all-in-one" + ], + "features": { + "ghcr.io/devcontainers/features/common-utils:1": {}, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers-contrib/features/yamllint:1": {}, + "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {} + }, + "remoteUser": "vscode", + "postCreateCommand": "bash ./.devcontainer/post-install.sh", + "mounts": [ + // "source=${localEnv:HOME}/.git_stop_words,target=/home/vscode/.git_stop_words,type=bind,consistency=cached", + // "source=${localEnv:HOME}/.gitconfig.local,target=/home/vscode/.gitconfig.local,type=bind,consistency=cached", + // "source=${localEnv:HOME}/tmp,target=/home/vscode/tmp,type=bind" + ] + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], +} diff --git a/.devcontainer/post-install.sh b/.devcontainer/post-install.sh new file mode 100755 index 0000000..a9b10da --- /dev/null +++ b/.devcontainer/post-install.sh @@ -0,0 +1,1015 @@ +#!/usr/bin/env bash + +_mainScript_() { + + APT_PACKAGES=( + bat + bc + build-essential + coreutils + curl + dnsutils + exa + fzf + git + git-extras + iftop + iotop + jq + less + libmagickwand-dev + libxml2-utils + lnav + lsof + nano + net-tools + openssh-server + p7zip-full + python3-pip + shellcheck + unzip + yamllint + wget + zsh + ) + + echo "" + header "Installing apt packages" + _execute_ "sudo apt-get update" + _execute_ "sudo apt-get upgrade -y" + for package in "${APT_PACKAGES[@]}"; do + _execute_ -p "sudo apt-get install -y \"${package}\"" + done + + if command -v batcat &>/dev/null; then + _execute_ -p "mkdir -p /home/vscode/.local/bin && ln -s /usr/bin/batcat /home/vscode/.local/bin/bat" + fi + + echo "" + header "Installing shfmt" + if ! command -v shfmt &>/dev/null; then + _execute_ "curl -sS https://webi.sh/shfmt | sh" + fi + + REPOS=( + "\"https://github.com/natelandau/dotfiles.git\" \"${HOME}/repos/dotfiles\"" + ) + header "Configuring Terminal environment" + mkdir -p "${HOME}/repos" + for r in "${REPOS[@]}"; do + REPO_DIR="$(echo "${r}" | awk 'BEGIN { FS = "\"" } ; { print $4 }')" + if [ -d "${REPO_DIR}" ]; then + info "${REPO_DIR} already exists" + else + _execute_ -p "git clone ${r}" + fi + done + + if [ -e "${HOME}/repos/dotfiles/install.sh" ]; then + _execute_ -pvs "${HOME}/repos/dotfiles/install.sh" + else + warning "Dotfiles install script not found" + fi + + echo "" + header "Configuring UTF8 locale" + _execute_ "sudo locale-gen en_US.UTF-8" + { + echo "" + echo "export LANG=en_US.UTF-8" + echo "export LANGUAGE=en_US.UTF-8" + echo "export LC_ALL=en_US.UTF-8" + } >>"${HOME}/.zshrc" + { + echo "" + echo "export LANG=en_US.UTF-8" + echo "export LANGUAGE=en_US.UTF-8" + echo "export LC_ALL=en_US.UTF-8" + } >>"${HOME}/.bash_profile" + + echo "" + header "Create alias to obsidian-metadata" + echo 'alias om="obsidian-metadata"' >>"${HOME}/.bash_profile" + echo 'alias om="obsidian-metadata"' >>"${HOME}/.zshrc" + + echo "" + header "Configuring Python environment" + if command -v python3 &>/dev/null; then + _execute_ -pv "pip install --upgrade pip" + _execute_ -pv "pip install -U \ + asciinema \ + black \ + commitizen \ + pre-commit \ + yamllint \ + detect-secrets \ + ruff \ + mypy" + else + warning "python 3 is not installed" + fi + + echo "" + header "Install virtual environment with poetry" + if command -v poetry &>/dev/null; then + pushd "/workspaces/obsidian-metadata" &>/dev/null + _execute_ -pv "poetry install" + venv_path="$(poetry env info --path)" + echo "" >>"/home/vscode/.zshrc" + echo "source ${venv_path}/bin/activate" >>"/home/vscode/.zshrc" + echo "" >>"/home/vscode/.bash_profile" + echo "source ${venv_path}/bin/activate" >>"/home/vscode/.bash_profile" + + popd &>/dev/null + else + warning "poetry is not installed" + fi + + echo "" + header "Initialize pre-commit" + if command -v pre-commit &>/dev/null; then + if [ -d "/workspaces/obsidian-metadata/.git" ]; then + pushd "/workspaces/obsidian-metadata" &>/dev/null + _execute_ -pv "pre-commit install --install-hooks" + _execute_ -pv "pre-commit autoupdate" + popd &>/dev/null + else + warning "Git repository not found in /workspaces/obsidian-metadata. Initialize pre-commit manually." + fi + else + warning "pre-commit is not installed" + fi + +} +# end _mainScript_ + +# ################################## Flags and defaults +# Required variables +LOGFILE="/home/vscode/logs/$(basename "$0").log" +QUIET=false +LOGLEVEL=ERROR +VERBOSE=false +FORCE=false +DRYRUN=false +declare -a ARGS=() + +# Script specific + +# ################################## Custom utility functions (Pasted from repository) +_execute_() { + # DESC: + # Executes commands while respecting global DRYRUN, VERBOSE, LOGGING, and QUIET flags + # ARGS: + # $1 (Required) - The command to be executed. Quotation marks MUST be escaped. + # $2 (Optional) - String to display after command is executed + # OPTS: + # -v Always print output from the execute function to STDOUT + # -n Use NOTICE level alerting (default is INFO) + # -p Pass a failed command with 'return 0'. This effectively bypasses set -e. + # -e Bypass _alert_ functions and use 'printf RESULT' + # -s Use '_alert_ success' for successful output. (default is 'info') + # -q Do not print output (QUIET mode) + # OUTS: + # stdout: Configurable output + # USE : + # _execute_ "cp -R \"~/dir/somefile.txt\" \"someNewFile.txt\"" "Optional message" + # _execute_ -sv "mkdir \"some/dir\"" + # NOTE: + # If $DRYRUN=true, no commands are executed and the command that would have been executed + # is printed to STDOUT using dryrun level alerting + # If $VERBOSE=true, the command's native output is printed to stdout. This can be forced + # with '_execute_ -v' + + local _localVerbose=false + local _passFailures=false + local _echoResult=false + local _echoSuccessResult=false + local _quietMode=false + local _echoNoticeResult=false + local opt + + local OPTIND=1 + while getopts ":vVpPeEsSqQnN" opt; do + case ${opt} in + v | V) _localVerbose=true ;; + p | P) _passFailures=true ;; + e | E) _echoResult=true ;; + s | S) _echoSuccessResult=true ;; + q | Q) _quietMode=true ;; + n | N) _echoNoticeResult=true ;; + *) + { + error "Unrecognized option '$1' passed to _execute_. Exiting." + _safeExit_ + } + ;; + esac + done + shift $((OPTIND - 1)) + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _command="${1}" + local _executeMessage="${2:-$1}" + + local _saveVerbose=${VERBOSE} + if "${_localVerbose}"; then + VERBOSE=true + fi + + if "${DRYRUN-}"; then + if "${_quietMode}"; then + VERBOSE=${_saveVerbose} + return 0 + fi + if [ -n "${2-}" ]; then + dryrun "${1} (${2})" "$(caller)" + else + dryrun "${1}" "$(caller)" + fi + elif ${VERBOSE-}; then + if eval "${_command}"; then + if "${_quietMode}"; then + VERBOSE=${_saveVerbose} + elif "${_echoResult}"; then + printf "%s\n" "${_executeMessage}" + elif "${_echoSuccessResult}"; then + success "${_executeMessage}" + elif "${_echoNoticeResult}"; then + notice "${_executeMessage}" + else + info "${_executeMessage}" + fi + else + if "${_quietMode}"; then + VERBOSE=${_saveVerbose} + elif "${_echoResult}"; then + printf "%s\n" "warning: ${_executeMessage}" + else + warning "${_executeMessage}" + fi + VERBOSE=${_saveVerbose} + "${_passFailures}" && return 0 || return 1 + fi + else + if eval "${_command}" >/dev/null 2>&1; then + if "${_quietMode}"; then + VERBOSE=${_saveVerbose} + elif "${_echoResult}"; then + printf "%s\n" "${_executeMessage}" + elif "${_echoSuccessResult}"; then + success "${_executeMessage}" + elif "${_echoNoticeResult}"; then + notice "${_executeMessage}" + else + info "${_executeMessage}" + fi + else + if "${_quietMode}"; then + VERBOSE=${_saveVerbose} + elif "${_echoResult}"; then + printf "%s\n" "error: ${_executeMessage}" + else + warning "${_executeMessage}" + fi + VERBOSE=${_saveVerbose} + "${_passFailures}" && return 0 || return 1 + fi + fi + VERBOSE=${_saveVerbose} + return 0 +} + +# ################################## Functions required for this template to work + +_setColors_() { + # DESC: + # Sets colors use for alerts. + # ARGS: + # None + # OUTS: + # None + # USAGE: + # printf "%s\n" "${blue}Some text${reset}" + + if tput setaf 1 >/dev/null 2>&1; then + bold=$(tput bold) + underline=$(tput smul) + reverse=$(tput rev) + reset=$(tput sgr0) + + if [[ $(tput colors) -ge 256 ]] >/dev/null 2>&1; then + white=$(tput setaf 231) + blue=$(tput setaf 38) + yellow=$(tput setaf 11) + green=$(tput setaf 82) + red=$(tput setaf 9) + purple=$(tput setaf 171) + gray=$(tput setaf 250) + else + white=$(tput setaf 7) + blue=$(tput setaf 38) + yellow=$(tput setaf 3) + green=$(tput setaf 2) + red=$(tput setaf 9) + purple=$(tput setaf 13) + gray=$(tput setaf 7) + fi + else + bold="\033[4;37m" + reset="\033[0m" + underline="\033[4;37m" + # shellcheck disable=SC2034 + reverse="" + white="\033[0;37m" + blue="\033[0;34m" + yellow="\033[0;33m" + green="\033[1;32m" + red="\033[0;31m" + purple="\033[0;35m" + gray="\033[0;37m" + fi +} + +_alert_() { + # DESC: + # Controls all printing of messages to log files and stdout. + # ARGS: + # $1 (required) - The type of alert to print + # (success, header, notice, dryrun, debug, warning, error, + # fatal, info, input) + # $2 (required) - The message to be printed to stdout and/or a log file + # $3 (optional) - Pass '${LINENO}' to print the line number where the _alert_ was triggered + # OUTS: + # stdout: The message is printed to stdout + # log file: The message is printed to a log file + # USAGE: + # [_alertType] "[MESSAGE]" "${LINENO}" + # NOTES: + # - The colors of each alert type are set in this function + # - For specified alert types, the funcstac will be printed + + local _color + local _alertType="${1}" + local _message="${2}" + local _line="${3-}" # Optional line number + + [[ $# -lt 2 ]] && fatal 'Missing required argument to _alert_' + + if [[ -n ${_line} && ${_alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}(line: ${_line}) $(_printFuncStack_)" + elif [[ -n ${_line} && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}(line: ${_line})" + elif [[ -z ${_line} && ${_alertType} =~ ^(fatal|error) && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}$(_printFuncStack_)" + fi + + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _color="${bold}${red}" + elif [ "${_alertType}" == "info" ]; then + _color="${gray}" + elif [ "${_alertType}" == "warning" ]; then + _color="${red}" + elif [ "${_alertType}" == "success" ]; then + _color="${green}" + elif [ "${_alertType}" == "debug" ]; then + _color="${purple}" + elif [ "${_alertType}" == "header" ]; then + _color="${bold}${white}${underline}" + elif [ "${_alertType}" == "notice" ]; then + _color="${bold}" + elif [ "${_alertType}" == "input" ]; then + _color="${bold}${underline}" + elif [ "${_alertType}" = "dryrun" ]; then + _color="${blue}" + else + _color="" + fi + + _writeToScreen_() { + ("${QUIET}") && return 0 # Print to console when script is not 'quiet' + [[ ${VERBOSE} == false && ${_alertType} =~ ^(debug|verbose) ]] && return 0 + + if ! [[ -t 1 || -z ${TERM-} ]]; then # Don't use colors on non-recognized terminals + _color="" + reset="" + fi + + if [[ ${_alertType} == header ]]; then + printf "${_color}%s${reset}\n" "${_message}" + else + printf "${_color}[%7s] %s${reset}\n" "${_alertType}" "${_message}" + fi + } + _writeToScreen_ + + _writeToLog_() { + [[ ${_alertType} == "input" ]] && return 0 + [[ ${LOGLEVEL} =~ (off|OFF|Off) ]] && return 0 + if [ -z "${LOGFILE-}" ]; then + LOGFILE="$(pwd)/$(basename "$0").log" + fi + [ ! -d "$(dirname "${LOGFILE}")" ] && mkdir -p "$(dirname "${LOGFILE}")" + [[ ! -f ${LOGFILE} ]] && touch "${LOGFILE}" + + # Don't use colors in logs + local _cleanmessage + _cleanmessage="$(printf "%s" "${_message}" | sed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')" + # Print message to log file + printf "%s [%7s] %s %s\n" "$(date +"%b %d %R:%S")" "${_alertType}" "[$(/bin/hostname)]" "${_cleanmessage}" >>"${LOGFILE}" + } + + # Write specified log level data to logfile + case "${LOGLEVEL:-ERROR}" in + ALL | all | All) + _writeToLog_ + ;; + DEBUG | debug | Debug) + _writeToLog_ + ;; + INFO | info | Info) + if [[ ${_alertType} =~ ^(error|fatal|warning|info|notice|success) ]]; then + _writeToLog_ + fi + ;; + NOTICE | notice | Notice) + if [[ ${_alertType} =~ ^(error|fatal|warning|notice|success) ]]; then + _writeToLog_ + fi + ;; + WARN | warn | Warn) + if [[ ${_alertType} =~ ^(error|fatal|warning) ]]; then + _writeToLog_ + fi + ;; + ERROR | error | Error) + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _writeToLog_ + fi + ;; + FATAL | fatal | Fatal) + if [[ ${_alertType} =~ ^fatal ]]; then + _writeToLog_ + fi + ;; + OFF | off) + return 0 + ;; + *) + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _writeToLog_ + fi + ;; + esac + +} # /_alert_ + +error() { _alert_ error "${1}" "${2-}"; } +warning() { _alert_ warning "${1}" "${2-}"; } +notice() { _alert_ notice "${1}" "${2-}"; } +info() { _alert_ info "${1}" "${2-}"; } +success() { _alert_ success "${1}" "${2-}"; } +dryrun() { _alert_ dryrun "${1}" "${2-}"; } +input() { _alert_ input "${1}" "${2-}"; } +header() { _alert_ header "${1}" "${2-}"; } +debug() { _alert_ debug "${1}" "${2-}"; } +fatal() { + _alert_ fatal "${1}" "${2-}" + _safeExit_ "1" +} + +_printFuncStack_() { + # DESC: + # Prints the function stack in use. Used for debugging, and error reporting. + # ARGS: + # None + # OUTS: + # stdout: Prints [function]:[file]:[line] + # NOTE: + # Does not print functions from the alert class + local _i + declare -a _funcStackResponse=() + for ((_i = 1; _i < ${#BASH_SOURCE[@]}; _i++)); do + case "${FUNCNAME[${_i}]}" in + _alert_ | _trapCleanup_ | fatal | error | warning | notice | info | debug | dryrun | header | success) + continue + ;; + *) + _funcStackResponse+=("${FUNCNAME[${_i}]}:$(basename "${BASH_SOURCE[${_i}]}"):${BASH_LINENO[_i - 1]}") + ;; + esac + + done + printf "( " + printf %s "${_funcStackResponse[0]}" + printf ' < %s' "${_funcStackResponse[@]:1}" + printf ' )\n' +} + +_safeExit_() { + # DESC: + # Cleanup and exit from a script + # ARGS: + # $1 (optional) - Exit code (defaults to 0) + # OUTS: + # None + + if [[ -d ${SCRIPT_LOCK-} ]]; then + if command rm -rf "${SCRIPT_LOCK}"; then + debug "Removing script lock" + else + warning "Script lock could not be removed. Try manually deleting ${yellow}'${SCRIPT_LOCK}'" + fi + fi + + if [[ -n ${TMP_DIR-} && -d ${TMP_DIR-} ]]; then + if [[ ${1-} == 1 && -n "$(ls "${TMP_DIR}")" ]]; then + command rm -r "${TMP_DIR}" + else + command rm -r "${TMP_DIR}" + debug "Removing temp directory" + fi + fi + + trap - INT TERM EXIT + exit "${1:-0}" +} + +_trapCleanup_() { + # DESC: + # Log errors and cleanup from script when an error is trapped. Called by 'trap' + # ARGS: + # $1: Line number where error was trapped + # $2: Line number in function + # $3: Command executing at the time of the trap + # $4: Names of all shell functions currently in the execution call stack + # $5: Scriptname + # $6: $BASH_SOURCE + # USAGE: + # trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' EXIT INT TERM SIGINT SIGQUIT SIGTERM ERR + # OUTS: + # Exits script with error code 1 + + local _line=${1-} # LINENO + local _linecallfunc=${2-} + local _command="${3-}" + local _funcstack="${4-}" + local _script="${5-}" + local _sourced="${6-}" + + # Replace the cursor in-case 'tput civis' has been used + tput cnorm + + if declare -f "fatal" &>/dev/null && declare -f "_printFuncStack_" &>/dev/null; then + + _funcstack="'$(printf "%s" "${_funcstack}" | sed -E 's/ / < /g')'" + + if [[ ${_script##*/} == "${_sourced##*/}" ]]; then + fatal "${7-} command: '${_command}' (line: ${_line}) [func: $(_printFuncStack_)]" + else + fatal "${7-} command: '${_command}' (func: ${_funcstack} called at line ${_linecallfunc} of '${_script##*/}') (line: ${_line} of '${_sourced##*/}') " + fi + else + printf "%s\n" "Fatal error trapped. Exiting..." + fi + + if declare -f _safeExit_ &>/dev/null; then + _safeExit_ 1 + else + exit 1 + fi +} + +_makeTempDir_() { + # DESC: + # Creates a temp directory to house temporary files + # ARGS: + # $1 (Optional) - First characters/word of directory name + # OUTS: + # Sets $TMP_DIR variable to the path of the temp directory + # USAGE: + # _makeTempDir_ "$(basename "$0")" + + [ -d "${TMP_DIR-}" ] && return 0 + + if [ -n "${1-}" ]; then + TMP_DIR="${TMPDIR:-/tmp/}${1}.${RANDOM}.${RANDOM}.$$" + else + TMP_DIR="${TMPDIR:-/tmp/}$(basename "$0").${RANDOM}.${RANDOM}.${RANDOM}.$$" + fi + (umask 077 && mkdir "${TMP_DIR}") || { + fatal "Could not create temporary directory! Exiting." + } + debug "\$TMP_DIR=${TMP_DIR}" +} + +# shellcheck disable=SC2120 +_acquireScriptLock_() { + # DESC: + # Acquire script lock to prevent running the same script a second time before the + # first instance exits + # ARGS: + # $1 (optional) - Scope of script execution lock (system or user) + # OUTS: + # exports $SCRIPT_LOCK - Path to the directory indicating we have the script lock + # Exits script if lock cannot be acquired + # NOTE: + # If the lock was acquired it's automatically released in _safeExit_() + + local _lockDir + if [[ ${1-} == 'system' ]]; then + _lockDir="${TMPDIR:-/tmp/}$(basename "$0").lock" + else + _lockDir="${TMPDIR:-/tmp/}$(basename "$0").${UID}.lock" + fi + + if command mkdir "${_lockDir}" 2>/dev/null; then + readonly SCRIPT_LOCK="${_lockDir}" + debug "Acquired script lock: ${yellow}${SCRIPT_LOCK}${purple}" + else + if declare -f "_safeExit_" &>/dev/null; then + error "Unable to acquire script lock: ${yellow}${_lockDir}${red}" + fatal "If you trust the script isn't running, delete the lock dir" + else + printf "%s\n" "ERROR: Could not acquire script lock. If you trust the script isn't running, delete: ${_lockDir}" + exit 1 + fi + + fi +} + +_setPATH_() { + # DESC: + # Add directories to $PATH so script can find executables + # ARGS: + # $@ - One or more paths + # OPTS: + # -x - Fail if directories are not found + # OUTS: + # 0: Success + # 1: Failure + # Adds items to $PATH + # USAGE: + # _setPATH_ "/usr/local/bin" "/home/vscode/bin" "$(npm bin)" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local opt + local OPTIND=1 + local _failIfNotFound=false + + while getopts ":xX" opt; do + case ${opt} in + x | X) _failIfNotFound=true ;; + *) + { + error "Unrecognized option '${1}' passed to _backupFile_" "${LINENO}" + return 1 + } + ;; + esac + done + shift $((OPTIND - 1)) + + local _newPath + + for _newPath in "$@"; do + if [ -d "${_newPath}" ]; then + if ! printf "%s" "${PATH}" | grep -Eq "(^|:)${_newPath}($|:)"; then + if PATH="${_newPath}:${PATH}"; then + debug "Added '${_newPath}' to PATH" + else + debug "'${_newPath}' already in PATH" + fi + else + debug "_setPATH_: '${_newPath}' already exists in PATH" + fi + else + debug "_setPATH_: can not find: ${_newPath}" + if [[ ${_failIfNotFound} == true ]]; then + return 1 + fi + continue + fi + done + return 0 +} + +_useGNUutils_() { + # DESC: + # Add GNU utilities to PATH to allow consistent use of sed/grep/tar/etc. on MacOS + # ARGS: + # None + # OUTS: + # 0 if successful + # 1 if unsuccessful + # PATH: Adds GNU utilities to the path + # USAGE: + # # if ! _useGNUUtils_; then exit 1; fi + # NOTES: + # GNU utilities can be added to MacOS using Homebrew + + ! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_" + + if _setPATH_ \ + "/usr/local/opt/gnu-tar/libexec/gnubin" \ + "/usr/local/opt/coreutils/libexec/gnubin" \ + "/usr/local/opt/gnu-sed/libexec/gnubin" \ + "/usr/local/opt/grep/libexec/gnubin" \ + "/usr/local/opt/findutils/libexec/gnubin" \ + "/opt/homebrew/opt/findutils/libexec/gnubin" \ + "/opt/homebrew/opt/gnu-sed/libexec/gnubin" \ + "/opt/homebrew/opt/grep/libexec/gnubin" \ + "/opt/homebrew/opt/coreutils/libexec/gnubin" \ + "/opt/homebrew/opt/gnu-tar/libexec/gnubin"; then + return 0 + else + return 1 + fi + +} + +_homebrewPath_() { + # DESC: + # Add homebrew bin dir to PATH + # ARGS: + # None + # OUTS: + # 0 if successful + # 1 if unsuccessful + # PATH: Adds homebrew bin directory to PATH + # USAGE: + # # if ! _homebrewPath_; then exit 1; fi + + ! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_" + + if _uname=$(command -v uname); then + if "${_uname}" | tr '[:upper:]' '[:lower:]' | grep -q 'darwin'; then + if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then + return 0 + else + return 1 + fi + fi + else + if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then + return 0 + else + return 1 + fi + fi +} + +_parseOptions_() { + # DESC: + # Iterates through options passed to script and sets variables. Will break -ab into -a -b + # when needed and --foo=bar into --foo bar + # ARGS: + # $@ from command line + # OUTS: + # Sets array 'ARGS' containing all arguments passed to script that were not parsed as options + # USAGE: + # _parseOptions_ "$@" + + # Iterate over options + local _optstring=h + declare -a _options + local _c + local i + while (($#)); do + case $1 in + # If option is of type -ab + -[!-]?*) + # Loop over each character starting with the second + for ((i = 1; i < ${#1}; i++)); do + _c=${1:i:1} + _options+=("-${_c}") # Add current char to options + # If option takes a required argument, and it's not the last char make + # the rest of the string its argument + if [[ ${_optstring} == *"${_c}:"* && -n ${1:i+1} ]]; then + _options+=("${1:i+1}") + break + fi + done + ;; + # If option is of type --foo=bar + --?*=*) _options+=("${1%%=*}" "${1#*=}") ;; + # add --endopts for -- + --) _options+=(--endopts) ;; + # Otherwise, nothing special + *) _options+=("$1") ;; + esac + shift + done + set -- "${_options[@]-}" + unset _options + + # Read the options and set stuff + # shellcheck disable=SC2034 + while [[ ${1-} == -?* ]]; do + case $1 in + # Custom options + + # Common options + -h | --help) + _usage_ + _safeExit_ + ;; + --loglevel) + shift + LOGLEVEL=${1} + ;; + --logfile) + shift + LOGFILE="${1}" + ;; + -n | --dryrun) DRYRUN=true ;; + -v | --verbose) VERBOSE=true ;; + -q | --quiet) QUIET=true ;; + --force) FORCE=true ;; + --endopts) + shift + break + ;; + *) + if declare -f _safeExit_ &>/dev/null; then + fatal "invalid option: $1" + else + printf "%s\n" "ERROR: Invalid option: $1" + exit 1 + fi + ;; + esac + shift + done + + if [[ -z ${*} || ${*} == null ]]; then + ARGS=() + else + ARGS+=("$@") # Store the remaining user input as arguments. + fi +} + +_columns_() { + # DESC: + # Prints a two column output from a key/value pair. + # Optionally pass a number of 2 space tabs to indent the output. + # ARGS: + # $1 (required): Key name (Left column text) + # $2 (required): Long value (Right column text. Wraps around if too long) + # $3 (optional): Number of 2 character tabs to indent the command (default 1) + # OPTS: + # -b Bold the left column + # -u Underline the left column + # -r Reverse background and foreground colors + # OUTS: + # stdout: Prints the output in columns + # NOTE: + # Long text or ANSI colors in the first column may create display issues + # USAGE: + # _columns_ "Key" "Long value text" [tab level] + + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local opt + local OPTIND=1 + local _style="" + while getopts ":bBuUrR" opt; do + case ${opt} in + b | B) _style="${_style}${bold}" ;; + u | U) _style="${_style}${underline}" ;; + r | R) _style="${_style}${reverse}" ;; + *) fatal "Unrecognized option '${1}' passed to ${FUNCNAME[0]}. Exiting." ;; + esac + done + shift $((OPTIND - 1)) + + local _key="${1}" + local _value="${2}" + local _tabLevel="${3-}" + local _tabSize=2 + local _line + local _rightIndent + local _leftIndent + if [[ -z ${3-} ]]; then + _tabLevel=0 + fi + + _leftIndent="$((_tabLevel * _tabSize))" + + local _leftColumnWidth="$((30 + _leftIndent))" + + if [ "$(tput cols)" -gt 180 ]; then + _rightIndent=110 + elif [ "$(tput cols)" -gt 160 ]; then + _rightIndent=90 + elif [ "$(tput cols)" -gt 130 ]; then + _rightIndent=60 + elif [ "$(tput cols)" -gt 120 ]; then + _rightIndent=50 + elif [ "$(tput cols)" -gt 110 ]; then + _rightIndent=40 + elif [ "$(tput cols)" -gt 100 ]; then + _rightIndent=30 + elif [ "$(tput cols)" -gt 90 ]; then + _rightIndent=20 + elif [ "$(tput cols)" -gt 80 ]; then + _rightIndent=10 + else + _rightIndent=0 + fi + + local _rightWrapLength=$(($(tput cols) - _leftColumnWidth - _leftIndent - _rightIndent)) + + local _first_line=0 + while read -r _line; do + if [[ ${_first_line} -eq 0 ]]; then + _first_line=1 + else + _key=" " + fi + printf "%-${_leftIndent}s${_style}%-${_leftColumnWidth}b${reset} %b\n" "" "${_key}${reset}" "${_line}" + done <<<"$(fold -w${_rightWrapLength} -s <<<"${_value}")" +} + +_usage_() { + cat < + Version range or exact version of a Python version to use, using SemVer's version range syntax. + required: false + default: 3.x + +outputs: + python-version: + description: The installed python version. Useful when given a version range as input. + value: ${{ steps.setup-python.outputs.python-version }} + cache-hit: + description: A boolean value to indicate projects dependencies were cached + value: ${{ steps.setup-python.outputs.cache-hit }} + poetry-cache-hit: + description: A boolean value to indicate Poetry installation was cached + value: ${{ steps.pipx-cache.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Get pipx env vars + id: pipx-env-vars + shell: bash + run: | + echo "pipx-home=${PIPX_HOME}" >> $GITHUB_OUTPUT + echo "pipx-bin-dir=${PIPX_BIN_DIR}" >> $GITHUB_OUTPUT + + - name: Load pipx cache + # If env vars are not defined do not load cache + if: > + steps.pipx-env-vars.outputs.pipx-home != '' + && steps.pipx-env-vars.outputs.pipx-bin-dir != '' + id: pipx-cache + uses: actions/cache@v3 + with: + path: | + ${{ steps.pipx-env-vars.outputs.pipx-home }}/venvs/poetry + ${{ steps.pipx-env-vars.outputs.pipx-bin-dir }}/poetry + key: ${{ runner.os }}-${{ inputs.python-version }}-pipx-${{ hashFiles('**/poetry.lock') }} + + - name: Install poetry + # If env vars are not defined or we missed pipx cache, install poetry + if: > + ( + steps.pipx-env-vars.outputs.pipx-home == '' + && steps.pipx-env-vars.outputs.pipx-bin-dir == '' + ) + || steps.pipx-cache.outputs.cache-hit != 'true' + shell: bash + run: pipx install poetry + + - name: Load poetry cache + uses: actions/setup-python@v4 + id: setup-python + with: + python-version: ${{ inputs.python-version }} + cache: poetry + + - name: Install poetry dependencies + # If we missed poetry cache install dependencies + if: steps.setup-python.outputs.cache-hit != 'true' + shell: bash + run: poetry install --all-extras diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a7ac900 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,23 @@ +--- +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + commit-message: + prefix: "ci" + prefix-development: "ci" + include: "scope" + - package-ecosystem: pip + directory: / + schedule: + interval: monthly + commit-message: + prefix: "build" + prefix-development: "build" + include: "scope" + versioning-strategy: lockfile-only + allow: + - dependency-type: "all" diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..a1064b6 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,21 @@ +--- +github_actions: + - ".github/**" +dev_container: + - ".devcontainer/**" +configuration: + - ".*" + - "*.js" + - "*.json" + - "*.toml" + - "*.yaml" + - "*.yml" +documentation: + - "**.md" + - "docs/**" + - LICENSE +python: + - "src/**" + - "tests/**" +dependencies: + - "*.lock" diff --git a/.github/workflows/commit-linter.yml b/.github/workflows/commit-linter.yml new file mode 100644 index 0000000..5205486 --- /dev/null +++ b/.github/workflows/commit-linter.yml @@ -0,0 +1,36 @@ +--- +name: Commit Linter + +on: + pull_request: + types: [opened, reopened] + push: + branches: + - main + +permissions: # added using https://github.com/step-security/secure-workflows + contents: read + +jobs: + lint-commits: + if: "!contains(github.event.head_commit.message, 'bump(release)')" + permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for wagoid/commitlint-github-action to get commits in PR + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0 + with: + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Lint commits + uses: wagoid/commitlint-github-action@v5 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml new file mode 100644 index 0000000..096e530 --- /dev/null +++ b/.github/workflows/create-release.yml @@ -0,0 +1,91 @@ +--- +name: Create Release + +on: + push: + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + +jobs: + autorelease: + name: Create Release + runs-on: "ubuntu-latest" + strategy: + fail-fast: true + matrix: + python-version: ["3.11"] + steps: + - uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0 + with: + egress-policy: block + disable-sudo: true + allowed-endpoints: > + api.github.com:443 + files.pythonhosted.org:443 + github.com:443 + install.python-poetry.org:443 + pypi.org:443 + python-poetry.org:443 + uploads.github.com:443 + + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Python and Poetry + uses: ./.github/actions/setup-poetry + + - name: Add version to environment vars + run: | + PROJECT_VERSION=$(poetry version --short) + echo "PROJECT_VERSION=$PROJECT_VERSION" >> $GITHUB_ENV + + # ---------------------------------------------- + # Confirm we did, in fact, update the version + # ---------------------------------------------- + + - name: Check if tag version matches project version + run: | + TAG=$(git describe HEAD --tags --abbrev=0) + echo $TAG + echo $PROJECT_VERSION + if [[ "$TAG" != "v$PROJECT_VERSION" ]]; then exit 1; fi + + # ---------------------------------------------- + # Generate release notes + # ---------------------------------------------- + + - name: Release Notes + run: git log $(git describe HEAD~ --tags --abbrev=0)..HEAD --pretty='format:* %h %s' --no-merges >> ".github/RELEASE-TEMPLATE.md" + + # ---------------------------------------------- + # Test and then build the package + # ---------------------------------------------- + - name: run poetry build + run: | + poetry run poetry check + poetry run coverage run + poetry build + + # ---------------------------------------------- + # Build draft release (Note: Will need to manually publish) + # ---------------------------------------------- + + - name: Create Release Draft + uses: softprops/action-gh-release@v1 + with: + body_path: ".github/RELEASE-TEMPLATE.md" + draft: true + files: | + dist/*-${{env.PROJECT_VERSION}}-py3-none-any.whl + dist/*-${{env.PROJECT_VERSION}}.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/devcontainer-checker.yml b/.github/workflows/devcontainer-checker.yml new file mode 100644 index 0000000..2398cb9 --- /dev/null +++ b/.github/workflows/devcontainer-checker.yml @@ -0,0 +1,60 @@ +--- +name: "Dev Container Checker" + +on: + workflow_dispatch: + pull_request: + types: [opened, reopened] + paths: + - ".devcontainer/**" + - ".github/workflows/devcontainer-checker.yml" + push: + paths: + - ".devcontainer/**" + - ".github/workflows/devcontainer-checker.yml" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dev-container-checker: + runs-on: ubuntu-latest + + steps: + - uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0 + with: + egress-policy: block + allowed-endpoints: > + api.snapcraft.io:443 + auth.docker.io:443 + centralus.data.mcr.microsoft.com:443 + deb.debian.org:443 + deb.debian.org:80 + dl.yarnpkg.com:443 + eastus.data.mcr.microsoft.com:443 + files.pythonhosted.org:443 + ghcr.io:443 + git.rootprojects.org:443 + github.com:443 + mcr.microsoft.com:443 + nodejs.org:443 + objects.githubusercontent.com:443 + pkg-containers.githubusercontent.com:443 + production.cloudflare.docker.com:443 + pypi.org:443 + registry-1.docker.io:443 + registry.npmjs.org:443 + webi.sh:443 + westcentralus.data.mcr.microsoft.com:443 + westus.data.mcr.microsoft.com:443 + + - name: Checkout + uses: actions/checkout@v3 + + - name: Build and run dev container task + uses: devcontainers/ci@v0.2 + with: + runCmd: | + poe lint + poe test diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..c352dbf --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,23 @@ +--- +name: Pull Request Labeler +on: + - pull_request_target + +jobs: + label: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0 + with: + egress-policy: block + allowed-endpoints: > + api.github.com:443 + github.com:443 + + - uses: actions/labeler@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-linter.yml b/.github/workflows/pr-linter.yml new file mode 100644 index 0000000..a7f4e35 --- /dev/null +++ b/.github/workflows/pr-linter.yml @@ -0,0 +1,53 @@ +--- +name: Pull Request Linter + +on: + pull_request_target: + types: + - opened + - edited + - synchronize + branches: + - main + +permissions: # added using https://github.com/step-security/secure-workflows + contents: read + +jobs: + lint: + permissions: + pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs + statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0 + with: + egress-policy: block + allowed-endpoints: > + api.github.com:443 + + - name: Lint Pull Request + uses: amannn/action-semantic-pull-request@v5 + with: + validateSingleCommit: true + wip: true + types: | + fix + feat + docs + style + refactor + perf + test + build + ci + requireScope: false + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" + didn't match the configured pattern. Please ensure that the subject + doesn't start with an uppercase character. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml new file mode 100644 index 0000000..d362586 --- /dev/null +++ b/.github/workflows/pypi-release.yml @@ -0,0 +1,48 @@ +--- +name: Publish to PyPi +on: + workflow_dispatch: + release: + types: + - published + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + publish-to-pypi: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + python-version: ["3.11"] + steps: + - uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0 + with: + egress-policy: audit + disable-sudo: true + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Python and Poetry + uses: ./.github/actions/setup-poetry + + # ---------------------------------------------- + # Test and then build the package + # ---------------------------------------------- + - name: run poetry build + run: | + poetry run poetry check + poetry run coverage run + + # ---------------------------------------------- + # Publish to PyPi + # ---------------------------------------------- + - name: Publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: | + poetry config pypi-token.pypi $PYPI_TOKEN + poetry publish --build diff --git a/.github/workflows/python-code-checker.yml b/.github/workflows/python-code-checker.yml new file mode 100644 index 0000000..df8bb69 --- /dev/null +++ b/.github/workflows/python-code-checker.yml @@ -0,0 +1,92 @@ +--- +name: "Python Code Checker" + +on: + workflow_dispatch: + push: + paths: + - ".github/workflows/python-code-checker.yml" + - ".github/actions/**" + - "src/**" + - "tests/**" + - "pyproject.toml" + - "poetry.lock" + pull_request: + types: [opened, reopened] + paths: + - ".github/workflows/python-code-checker.yml" + - ".github/actions/**" + - "src/**" + - "tests/**" + - "pyproject.toml" + - "poetry.lock" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-python-code: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0 + with: + egress-policy: block + disable-sudo: true + allowed-endpoints: > + api.snapcraft.io:443 + api.github.com:443 + codecov.io:443 + files.pythonhosted.org:443 + github.com:443 + install.python-poetry.org:443 + pypi.org:443 + python-poetry.org:443 + storage.googleapis.com:443 + uploader.codecov.io:443 + + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Python and Poetry + uses: ./.github/actions/setup-poetry + + # ---------------------------------------------- + # run linters + # ---------------------------------------------- + + - name: Lint with Mypy + run: poetry run mypy src/ + - name: lint with ruff + run: poetry run ruff --extend-ignore=I001,D301 src/ + - name: check pyproject.toml + run: poetry run poetry check + - name: lint with black + run: poetry run black --check src/ + - name: run vulture + run: poetry run vulture src/ + - name: run interrogate + run: poetry run interrogate -c pyproject.toml . + + # ---------------------------------------------- + # run test suite + # ---------------------------------------------- + - name: Run tests with pytest + run: | + poetry run coverage run + poetry run coverage report + poetry run coverage xml + # ---------------------------------------------- + # upload coverage stats + # ---------------------------------------------- + - name: Upload coverage + if: github.ref == 'refs/heads/main' && matrix.python-version == '3.11' + uses: codecov/codecov-action@v3 + with: + # token: ${{ secrets.CODECOV_TOKEN }} # Only required for private repositories + files: reports/coverage.xml + fail_ci_if_error: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..556d11f --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Scratch folder for temporary files +scratch/ + +# Caches +*_cache/ +__pycache__/ + +# Coverage.py +htmlcov/ +reports/ + +# cruft +*.rej + +# Data +*.csv* +*.dat* +*.pickle* +*.xls* +*.zip* + +.envrc +.env + +# Jupyter +*.ipynb +.ipynb_checkpoints/ +notebooks/ + +# macOS +.DS_Store + +# mypy +.dmypy.json + +# Node.js +node_modules/ + +# Poetry +.venv/ +dist/ + +# PyCharm +.idea/ + +# pyenv +.python-version + +# Python +*.py[cdo] + +# act run workflows locally +bin/act diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..66148dd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,125 @@ +--- +# https://pre-commit.com +default_install_hook_types: [commit-msg, pre-commit] +default_stages: [commit, manual] +fail_fast: true +repos: + - repo: "https://github.com/commitizen-tools/commitizen" + rev: v2.39.1 + hooks: + - id: commitizen + - id: commitizen-branch + stages: + - post-commit + - push + + - repo: "https://github.com/pre-commit/pygrep-hooks" + rev: v1.10.0 + hooks: + - id: python-check-mock-methods + - id: python-no-eval + - id: python-no-log-warn + - id: python-use-type-annotations + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + - id: text-unicode-replacement-char + + - repo: "https://github.com/pre-commit/pre-commit-hooks" + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-json + exclude: .devcontainer/|.vscode/ + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: trailing-whitespace + types: [python] + args: [--markdown-linebreak-ext=md] + - id: end-of-file-fixer + types: [python] + + - repo: "https://github.com/adrienverge/yamllint.git" + rev: v1.29.0 + hooks: + - id: yamllint + files: ^.*\.(yaml|yml)$ + entry: yamllint --strict --config-file .yamllint.yml + + - repo: "https://github.com/charliermarsh/ruff-pre-commit" + rev: "v0.0.229" + hooks: + - id: ruff + args: ["--extend-ignore", "I001,D301,D401,PLR2004"] + + - repo: "https://github.com/jendrikseipp/vulture" + rev: "v2.7" + hooks: + - id: vulture + + - repo: local + hooks: + - id: custom + name: custom pre-commit script + entry: scripts/pre-commit-hook.sh + language: system + + - id: black + name: black + entry: black + require_serial: true + language: system + types: [python] + + - id: shellcheck + name: shellcheck + entry: shellcheck --check-sourced --severity=warning + language: system + types: [shell] + + - id: poetry-check + name: poetry check + entry: poetry check + language: system + files: pyproject.toml + pass_filenames: false + + - id: interrogate + name: interrogate check + entry: interrogate -c pyproject.toml src/ + language: system + types: [python] + pass_filenames: false + + - id: mypy + name: mypy + entry: mypy --config-file pyproject.toml + exclude: tests/ + language: system + types: [python] + + - id: pytest + name: pytest + entry: poe test + language: system + pass_filenames: false + files: | + (?x)^( + src/| + tests/| + poetry\.lock| + pyproject\.toml + ) diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..df215a8 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: CLi application", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/src/obsidian_metadata/cli.py", + "args": [ + "--config-file", + "${userHome}/.obsidian_metadata.toml", + ], + "console": "integratedTerminal", + "justMyCode": true + } + ] +} diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..ccd4c85 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,33 @@ +--- +# Find full documentation at: https://yamllint.readthedocs.io/en/stable/index.html +extends: default +locale: en_US.UTF-8 + +ignore: | + .venv + +rules: + braces: + level: error + max-spaces-inside: 1 + min-spaces-inside: 1 + comments-indentation: disable + comments: + min-spaces-from-content: 1 + indentation: + spaces: consistent + indent-sequences: true + check-multi-line-strings: false + line-length: disable + quoted-strings: + quote-type: any + required: false + extra-required: + - "^http://" + - "^https://" + - "ftp://" + - 'ssh \w.*' + extra-allowed: [] + truthy: + level: error + check-keys: false diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e4231b --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +[![Python Code Checker](https://github.com/natelandau/obsidian-metadata/actions/workflows/python-code-checker.yml/badge.svg)](https://github.com/natelandau/obsidian-metadata/actions/workflows/python-code-checker.yml) [![codecov](https://codecov.io/gh/natelandau/obsidian-metadata/branch/main/graph/badge.svg?token=3F2R43SSX4)](https://codecov.io/gh/natelandau/obsidian-metadata) +# obsidian-metadata +A script to make batch updates to metadata in an Obsidian vault. Provides the following capabilities: + +- in-text tag: delete every occurrence +- in-text tags: Rename tag (`#tag1` -> `#tag2`) +- frontmatter: Delete a key matching a regex pattern and all associated values +- frontmatter: Rename a key +- frontmatter: Delete a value matching a regex pattern from a specified key +- frontmatter: Rename a value from a specified key +- inline metadata: Delete a key matching a regex pattern and all associated values +- inline metadata: Rename a key +- inline metadata: Delete a value matching a regex pattern from a specified key +- inline metadata: Rename a value from a specified key +- vault: Create a backup of the Obsidian vault + + +## Install +`obsidian-metadata` requires Python v3.10 or above. + + +Use [PIPX](https://pypa.github.io/pipx/) to install this package from Github. + +```bash +pipx install git+https://${GITHUB_TOKEN}@github.com/natelandau/obsidian-metadata +``` + + +## Disclaimer +**Important:** It is strongly recommended that you back up your vault prior to committing changes. This script makes changes directly to the markdown files in your vault. Once the changes are committed, there is no ability to recreate the original information unless you have a backup. Follow the instructions in the script to create a backup of your vault if needed. + +The author of this script is not responsible for any data loss that may occur. Use at your own risk. + +## Usage +The script provides a menu of available actions. Make as many changes as you require and review them as you go. No changes are made to the Vault until they are explicitly committed. + +[![asciicast](https://asciinema.org/a/553464.svg)](https://asciinema.org/a/553464) + + +### Configuration +`obsidian-metadata` requires a configuration file at `~/.obsidian_metadata.toml`. On first run, this file will be created. Read the comments in this file to configure your preferences. This configuration file contains the following information. + +```toml +# Path to your obsidian vault +vault = "/path/to/vault" + +# Folders within the vault to ignore when indexing metadata +exclude_paths = [".git", ".obsidian"] +``` + + + +# Contributing + +## Setup: Once per project + +There are two ways to contribute to this project. + +### 21. Containerized development (Recommended) + +1. Clone this repository. `git clone https://github.com/natelandau/obsidian-metadata` +2. Open the repository in Visual Studio Code +3. Start the [Dev Container](https://code.visualstudio.com/docs/remote/containers). Run Ctrl/⌘ + + P → _Remote-Containers: Reopen in Container_. +4. Run `poetry env info -p` to find the PATH to the Python interpreter if needed by VSCode. + +### 2. Local development + +1. Install Python 3.10 and [Poetry](https://python-poetry.org) +2. Clone this repository. `git clone https://github.com/natelandau/obsidian-metadata` +3. Install the Poetry environment with `poetry install`. +4. Activate your Poetry environment with `poetry shell`. +5. Install the pre-commit hooks with `pre-commit install --install-hooks`. + +## Developing + +- This project follows the [Conventional Commits](https://www.conventionalcommits.org/) standard to automate [Semantic Versioning](https://semver.org/) and [Keep A Changelog](https://keepachangelog.com/) with [Commitizen](https://github.com/commitizen-tools/commitizen). + - When you're ready to commit changes run `cz c` +- Run `poe` from within the development environment to print a list of [Poe the Poet](https://github.com/nat-n/poethepoet) tasks available to run on this project. Common commands: + - `poe lint` runs all linters + - `poe test` runs all tests with Pytest +- Run `poetry add {package}` from within the development environment to install a run time dependency and add it to `pyproject.toml` and `poetry.lock`. +- Run `poetry remove {package}` from within the development environment to uninstall a run time dependency and remove it from `pyproject.toml` and `poetry.lock`. +- Run `poetry update` from within the development environment to upgrade all dependencies to the latest versions allowed by `pyproject.toml`. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..9b06d03 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,14 @@ +--- +coverage: + status: + project: + default: + target: 50% # the required coverage value + threshold: 1% # the leniency in hitting the target + + ignore: + - tests/ + +comment: + layout: "reach, diff, flags, files" # Remove items here to change the format + require_changes: true diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..6882a94 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1317 @@ +[[package]] +name = "absolufy-imports" +version = "0.3.1" +description = "A tool to automatically replace relative imports with absolute ones." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "argcomplete" +version = "2.0.0" +description = "Bash tab completion for argparse" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +test = ["coverage", "flake8", "pexpect", "wheel"] + +[[package]] +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "22.12.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "commitizen" +version = "2.39.1" +description = "Python commitizen client tool" +category = "dev" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +argcomplete = ">=1.12.1,<2.1" +charset-normalizer = ">=2.1.0,<3.0.0" +colorama = ">=0.4.1,<0.5.0" +decli = ">=0.5.2,<0.6.0" +jinja2 = ">=2.10.3" +packaging = ">=19" +pyyaml = ">=3.08" +questionary = ">=1.4.0,<2.0.0" +termcolor = {version = ">=1.1,<3", markers = "python_version >= \"3.7\""} +tomlkit = ">=0.5.3,<1.0.0" +typing-extensions = ">=4.0.1,<5.0.0" + +[[package]] +name = "coverage" +version = "7.0.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "decli" +version = "0.5.2" +description = "Minimal, easy-to-use, declarative cli tool" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "distlib" +version = "0.3.6" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + +[[package]] +name = "filelock" +version = "3.9.0" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "flake8" +version = "6.0.0" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.8.1" + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.10.0,<2.11.0" +pyflakes = ">=3.0.0,<3.1.0" + +[[package]] +name = "identify" +version = "2.5.13" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "interrogate" +version = "1.5.0" +description = "Interrogate a codebase for docstring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = "*" +click = ">=7.1" +colorama = "*" +py = "*" +tabulate = "*" +toml = "*" + +[package.extras] +dev = ["cairosvg", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "sphinx", "sphinx-autobuild", "wheel"] +docs = ["sphinx", "sphinx-autobuild"] +png = ["cairosvg"] +tests = ["pytest", "pytest-cov", "pytest-mock"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] + +[[package]] +name = "markdown-it-py" +version = "2.1.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] +code-style = ["pre-commit (==2.6)"] +compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mypy" +version = "0.991" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pathspec" +version = "0.10.3" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pdoc" +version = "12.3.1" +description = "API Documentation for Python Projects" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +Jinja2 = ">=2.11.0" +MarkupSafe = "*" +pygments = ">=2.12.0" + +[package.extras] +dev = ["black", "hypothesis", "mypy", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"] + +[[package]] +name = "pep8-naming" +version = "0.13.3" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=5.0.0" + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "poethepoet" +version = "0.18.1" +description = "A task runner that works well with poetry." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "pprintpp" +version = "0.4.0" +description = "A drop-in replacement for pprint that's actually pretty" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prompt-toolkit" +version = "3.0.36" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.10.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyflakes" +version = "3.0.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pygments" +version = "2.14.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pytest" +version = "7.2.1" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-clarity" +version = "1.0.1" +description = "A plugin providing an alternative, colourful diff output for failing assertions." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pprintpp = ">=0.4.0" +pytest = ">=3.5.0" +rich = ">=8.0.0" + +[[package]] +name = "pytest-mock" +version = "3.10.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-pretty-terminal" +version = "1.1.0" +description = "pytest plugin for generating prettier terminal output" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +pytest = ">=3.4.1" + +[package.extras] +test = ["pytest-adaptavist (>=5.1.1)"] + +[[package]] +name = "pytest-xdist" +version = "3.1.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "questionary" +version = "1.10.0" +description = "Python library to build pretty command line user prompts ⭐️" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + +[package.extras] +docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] + +[[package]] +name = "rich" +version = "13.2.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +markdown-it-py = ">=2.1.0,<3.0.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "ruamel-yaml" +version = "0.17.21" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +category = "main" +optional = false +python-versions = ">=3" + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.6", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.11\""} + +[package.extras] +docs = ["ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.7" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "ruff" +version = "0.0.217" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "setuptools" +version = "66.1.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "shellingham" +version = "1.5.0.post1" +description = "Tool to Detect Surrounding Shell" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "termcolor" +version = "2.2.0" +description = "ANSI color formatting for output in terminal" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tomlkit" +version = "0.11.6" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typeguard" +version = "2.13.3" +description = "Run-time type checker for Python" +category = "dev" +optional = false +python-versions = ">=3.5.3" + +[package.extras] +doc = ["sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["mypy", "pytest", "typing-extensions"] + +[[package]] +name = "typer" +version = "0.7.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +click = ">=7.1.1,<9.0.0" + +[package.extras] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] + +[[package]] +name = "types-python-dateutil" +version = "2.8.19.6" +description = "Typing stubs for python-dateutil" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-pyyaml" +version = "6.0.12.3" +description = "Typing stubs for PyYAML" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "4.4.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "virtualenv" +version = "20.17.1" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "vulture" +version = "2.7" +description = "Find dead code" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +toml = "*" + +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "1.1" + python-versions = "^3.10" +content-hash = "db4a41f0b94c3bf84fa548a6733da1cced1fedc2bdfdc6f80396dddbd5619bfd" + +[metadata.files] +absolufy-imports = [ + {file = "absolufy_imports-0.3.1-py2.py3-none-any.whl", hash = "sha256:49bf7c753a9282006d553ba99217f48f947e3eef09e18a700f8a82f75dc7fc5c"}, + {file = "absolufy_imports-0.3.1.tar.gz", hash = "sha256:c90638a6c0b66826d1fb4880ddc20ef7701af34192c94faf40b95d32b59f9793"}, +] +argcomplete = [ + {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, + {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, +] +attrs = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] +black = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +commitizen = [ + {file = "commitizen-2.39.1-py3-none-any.whl", hash = "sha256:2678c51ed38676435a4ba02e164605b0aacfefcc3f7e0c8d11dd39e367e20577"}, + {file = "commitizen-2.39.1.tar.gz", hash = "sha256:1f4b77a6b6cf43fc75e7fc604081add66026a5031c2a5032b2b9e8202bc57d47"}, +] +coverage = [ + {file = "coverage-7.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b"}, + {file = "coverage-7.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89"}, + {file = "coverage-7.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40"}, + {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2"}, + {file = "coverage-7.0.5-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289"}, + {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0"}, + {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8"}, + {file = "coverage-7.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809"}, + {file = "coverage-7.0.5-cp310-cp310-win32.whl", hash = "sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21"}, + {file = "coverage-7.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad"}, + {file = "coverage-7.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca"}, + {file = "coverage-7.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0"}, + {file = "coverage-7.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196"}, + {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0"}, + {file = "coverage-7.0.5-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc"}, + {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45"}, + {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757"}, + {file = "coverage-7.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095"}, + {file = "coverage-7.0.5-cp311-cp311-win32.whl", hash = "sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831"}, + {file = "coverage-7.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea"}, + {file = "coverage-7.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c"}, + {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7"}, + {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96"}, + {file = "coverage-7.0.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029"}, + {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"}, + {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2"}, + {file = "coverage-7.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47"}, + {file = "coverage-7.0.5-cp37-cp37m-win32.whl", hash = "sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882"}, + {file = "coverage-7.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d"}, + {file = "coverage-7.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb"}, + {file = "coverage-7.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6"}, + {file = "coverage-7.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd"}, + {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a"}, + {file = "coverage-7.0.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2"}, + {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7"}, + {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0"}, + {file = "coverage-7.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499"}, + {file = "coverage-7.0.5-cp38-cp38-win32.whl", hash = "sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16"}, + {file = "coverage-7.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af"}, + {file = "coverage-7.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab"}, + {file = "coverage-7.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637"}, + {file = "coverage-7.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4"}, + {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded"}, + {file = "coverage-7.0.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b"}, + {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209"}, + {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78"}, + {file = "coverage-7.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1"}, + {file = "coverage-7.0.5-cp39-cp39-win32.whl", hash = "sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904"}, + {file = "coverage-7.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f"}, + {file = "coverage-7.0.5-pp37.pp38.pp39-none-any.whl", hash = "sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0"}, + {file = "coverage-7.0.5.tar.gz", hash = "sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45"}, +] +decli = [ + {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, + {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, +] +distlib = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] +execnet = [ + {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, + {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, +] +filelock = [ + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, +] +flake8 = [ + {file = "flake8-6.0.0-py2.py3-none-any.whl", hash = "sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7"}, + {file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"}, +] +identify = [ + {file = "identify-2.5.13-py2.py3-none-any.whl", hash = "sha256:8aa48ce56e38c28b6faa9f261075dea0a942dfbb42b341b4e711896cbb40f3f7"}, + {file = "identify-2.5.13.tar.gz", hash = "sha256:abb546bca6f470228785338a01b539de8a85bbf46491250ae03363956d8ebb10"}, +] +iniconfig = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] +interrogate = [ + {file = "interrogate-1.5.0-py3-none-any.whl", hash = "sha256:a4ccc5cbd727c74acc98dee6f5e79ef264c0bcfa66b68d4e123069b2af89091a"}, + {file = "interrogate-1.5.0.tar.gz", hash = "sha256:b6f325f0aa84ac3ac6779d8708264d366102226c5af7d69058cecffcff7a6d6c"}, +] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +loguru = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] +markdown-it-py = [ + {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, + {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] +mccabe = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] +mdurl = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] +mypy = [ + {file = "mypy-0.991-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab"}, + {file = "mypy-0.991-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d"}, + {file = "mypy-0.991-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6"}, + {file = "mypy-0.991-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb"}, + {file = "mypy-0.991-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305"}, + {file = "mypy-0.991-cp310-cp310-win_amd64.whl", hash = "sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372"}, + {file = "mypy-0.991-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f"}, + {file = "mypy-0.991-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33"}, + {file = "mypy-0.991-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05"}, + {file = "mypy-0.991-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad"}, + {file = "mypy-0.991-cp311-cp311-win_amd64.whl", hash = "sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297"}, + {file = "mypy-0.991-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813"}, + {file = "mypy-0.991-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711"}, + {file = "mypy-0.991-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd"}, + {file = "mypy-0.991-cp37-cp37m-win_amd64.whl", hash = "sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a"}, + {file = "mypy-0.991-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93"}, + {file = "mypy-0.991-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf"}, + {file = "mypy-0.991-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135"}, + {file = "mypy-0.991-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70"}, + {file = "mypy-0.991-cp38-cp38-win_amd64.whl", hash = "sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d"}, + {file = "mypy-0.991-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5"}, + {file = "mypy-0.991-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3"}, + {file = "mypy-0.991-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648"}, + {file = "mypy-0.991-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476"}, + {file = "mypy-0.991-cp39-cp39-win_amd64.whl", hash = "sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461"}, + {file = "mypy-0.991-py3-none-any.whl", hash = "sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb"}, + {file = "mypy-0.991.tar.gz", hash = "sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] +packaging = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] +pastel = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] +pathspec = [ + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, +] +pdoc = [ + {file = "pdoc-12.3.1-py3-none-any.whl", hash = "sha256:c3f24f31286e634de9c76fa6e67bd5c0c5e74360b41dc91e6b82499831eb52d8"}, + {file = "pdoc-12.3.1.tar.gz", hash = "sha256:453236f225feddb8a9071428f1982a78d74b9b3da4bc4433aedb64dbd0cc87ab"}, +] +pep8-naming = [ + {file = "pep8-naming-0.13.3.tar.gz", hash = "sha256:1705f046dfcd851378aac3be1cd1551c7c1e5ff363bacad707d43007877fa971"}, + {file = "pep8_naming-0.13.3-py3-none-any.whl", hash = "sha256:1a86b8c71a03337c97181917e2b472f0f5e4ccb06844a0d6f0a33522549e7a80"}, +] +platformdirs = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +poethepoet = [ + {file = "poethepoet-0.18.1-py3-none-any.whl", hash = "sha256:e85727bf6f4a10bf6c1a43026bdeb40df689bea3c4682d03cbe531cabc8f2ba6"}, + {file = "poethepoet-0.18.1.tar.gz", hash = "sha256:5f3566b14c2f5dccdfbc3bb26f0096006b38dc0b9c74bd4f8dd1eba7b0e29f6a"}, +] +pprintpp = [ + {file = "pprintpp-0.4.0-py2.py3-none-any.whl", hash = "sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d"}, + {file = "pprintpp-0.4.0.tar.gz", hash = "sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403"}, +] +pre-commit = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [ + {file = "pycodestyle-2.10.0-py2.py3-none-any.whl", hash = "sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"}, + {file = "pycodestyle-2.10.0.tar.gz", hash = "sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053"}, +] +pyflakes = [ + {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, + {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, +] +pygments = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] +pytest = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] +pytest-clarity = [ + {file = "pytest-clarity-1.0.1.tar.gz", hash = "sha256:505fe345fad4fe11c6a4187fe683f2c7c52c077caa1e135f3e483fe112db7772"}, +] +pytest-mock = [ + {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, + {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, +] +pytest-pretty-terminal = [ + {file = "pytest-pretty-terminal-1.1.0.tar.gz", hash = "sha256:3a284bfaee4dac65524762436bbd875d70ca2dd1384170ef212887f860068f8f"}, + {file = "pytest_pretty_terminal-1.1.0-py3-none-any.whl", hash = "sha256:fe40d237383efe2d023f2b4faea9784da7f81c8b54987261ec5bb11966b98e77"}, +] +pytest-xdist = [ + {file = "pytest-xdist-3.1.0.tar.gz", hash = "sha256:40fdb8f3544921c5dfcd486ac080ce22870e71d82ced6d2e78fa97c2addd480c"}, + {file = "pytest_xdist-3.1.0-py3-none-any.whl", hash = "sha256:70a76f191d8a1d2d6be69fc440cdf85f3e4c03c08b520fd5dc5d338d6cf07d89"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +questionary = [ + {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, + {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, +] +rich = [ + {file = "rich-13.2.0-py3-none-any.whl", hash = "sha256:7c963f0d03819221e9ac561e1bc866e3f95a02248c1234daa48954e6d381c003"}, + {file = "rich-13.2.0.tar.gz", hash = "sha256:f1a00cdd3eebf999a15d85ec498bfe0b1a77efe9b34f645768a54132ef444ac5"}, +] +ruamel-yaml = [ + {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, + {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, +] +ruamel-yaml-clib = [ + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5859983f26d8cd7bb5c287ef452e8aacc86501487634573d260968f753e1d71"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:debc87a9516b237d0466a711b18b6ebeb17ba9f391eb7f91c649c5c4ec5006c7"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:df5828871e6648db72d1c19b4bd24819b80a755c4541d3409f0f7acd0f335c80"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:efa08d63ef03d079dcae1dfe334f6c8847ba8b645d08df286358b1f5293d24ab"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win32.whl", hash = "sha256:763d65baa3b952479c4e972669f679fe490eee058d5aa85da483ebae2009d231"}, + {file = "ruamel.yaml.clib-0.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:d000f258cf42fec2b1bbf2863c61d7b8918d31ffee905da62dede869254d3b8a"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:045e0626baf1c52e5527bd5db361bc83180faaba2ff586e763d3d5982a876a9e"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-macosx_12_6_arm64.whl", hash = "sha256:721bc4ba4525f53f6a611ec0967bdcee61b31df5a56801281027a3a6d1c2daf5"}, + {file = "ruamel.yaml.clib-0.2.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:41d0f1fa4c6830176eef5b276af04c89320ea616655d01327d5ce65e50575c94"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4b3a93bb9bc662fc1f99c5c3ea8e623d8b23ad22f861eb6fce9377ac07ad6072"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-macosx_12_0_arm64.whl", hash = "sha256:a234a20ae07e8469da311e182e70ef6b199d0fbeb6c6cc2901204dd87fb867e8"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:15910ef4f3e537eea7fe45f8a5d19997479940d9196f357152a09031c5be59f3"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:370445fd795706fd291ab00c9df38a0caed0f17a6fb46b0f607668ecb16ce763"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win32.whl", hash = "sha256:ecdf1a604009bd35c674b9225a8fa609e0282d9b896c03dd441a91e5f53b534e"}, + {file = "ruamel.yaml.clib-0.2.7-cp36-cp36m-win_amd64.whl", hash = "sha256:f34019dced51047d6f70cb9383b2ae2853b7fc4dce65129a5acd49f4f9256646"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2aa261c29a5545adfef9296b7e33941f46aa5bbd21164228e833412af4c9c75f"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f01da5790e95815eb5a8a138508c01c758e5f5bc0ce4286c4f7028b8dd7ac3d0"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:40d030e2329ce5286d6b231b8726959ebbe0404c92f0a578c0e2482182e38282"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c3ca1fbba4ae962521e5eb66d72998b51f0f4d0f608d3c0347a48e1af262efa7"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win32.whl", hash = "sha256:7bdb4c06b063f6fd55e472e201317a3bb6cdeeee5d5a38512ea5c01e1acbdd93"}, + {file = "ruamel.yaml.clib-0.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:be2a7ad8fd8f7442b24323d24ba0b56c51219513cfa45b9ada3b87b76c374d4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:91a789b4aa0097b78c93e3dc4b40040ba55bef518f84a40d4442f713b4094acb"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:99e77daab5d13a48a4054803d052ff40780278240a902b880dd37a51ba01a307"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3243f48ecd450eddadc2d11b5feb08aca941b5cd98c9b1db14b2fd128be8c697"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8831a2cedcd0f0927f788c5bdf6567d9dc9cc235646a434986a852af1cb54b4b"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win32.whl", hash = "sha256:3110a99e0f94a4a3470ff67fc20d3f96c25b13d24c6980ff841e82bafe827cac"}, + {file = "ruamel.yaml.clib-0.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:92460ce908546ab69770b2e576e4f99fbb4ce6ab4b245345a3869a0a0410488f"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5bc0667c1eb8f83a3752b71b9c4ba55ef7c7058ae57022dd9b29065186a113d9"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:4a4d8d417868d68b979076a9be6a38c676eca060785abaa6709c7b31593c35d1"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bf9a6bc4a0221538b1a7de3ed7bca4c93c02346853f44e1cd764be0023cd3640"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7b301ff08055d73223058b5c46c55638917f04d21577c95e00e0c4d79201a6b"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win32.whl", hash = "sha256:d5e51e2901ec2366b79f16c2299a03e74ba4531ddcfacc1416639c557aef0ad8"}, + {file = "ruamel.yaml.clib-0.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:184faeaec61dbaa3cace407cffc5819f7b977e75360e8d5ca19461cd851a5fc5"}, + {file = "ruamel.yaml.clib-0.2.7.tar.gz", hash = "sha256:1f08fd5a2bea9c4180db71678e850b995d2a5f4537be0e94557668cf0f5f9497"}, +] +ruff = [ + {file = "ruff-0.0.217-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:d0140fd3c1a254cca6ac9e5794b5d6090145dc6a3864a49b871c919fe2f7bbb7"}, + {file = "ruff-0.0.217-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:828f7cca24f8ccae160593094af355672f0f76d2738b31f85be9671301296136"}, + {file = "ruff-0.0.217-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ba8a78f16ddbe26b8833f06e6e679e56ca0ea1fce4c2f322ba995e3fce7557"}, + {file = "ruff-0.0.217-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b12cf86a3f59127f592c49107761d45d36861bb82fdaae90246b308bdbbd7e10"}, + {file = "ruff-0.0.217-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d7cf9468112e18effc53fef291e7186d2deefabbe0924612c51f5a1f45dab6a"}, + {file = "ruff-0.0.217-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4060479b55e565755aa1c2b66daf6ac59770d266c86ee90d32e5c97ad9fc58b9"}, + {file = "ruff-0.0.217-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44f9b638628ca7625f0b1ea3e4073a08c7c8cfa1f1bee6693cbab1b9b5e38945"}, + {file = "ruff-0.0.217-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d6d01bb1b4d9ef48fd2bdff714e222589fa0c779f20befeef8843dad1833320"}, + {file = "ruff-0.0.217-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae536b48b81aab1b2119b0988cdbb48728c965cfa1fda4ec285187b15e75a09e"}, + {file = "ruff-0.0.217-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:35c77fc925d4b7b3f2ec47e7136e084b046b5ccdd39149f2df51ed3675b4fd1f"}, + {file = "ruff-0.0.217-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c02ef082b697a8aa8725d9c519cd9199f8ee2cbfb43e43d25d330e418b22fe3"}, + {file = "ruff-0.0.217-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3d15ac221c5706981b376cb563aeacccf58c4903199f936f168791d0c2cd9bc4"}, + {file = "ruff-0.0.217-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5bbbee140e73f38b43fae8a13bba2ea38398eed4413ecec6d18a95a3679dbe2"}, + {file = "ruff-0.0.217-py3-none-win32.whl", hash = "sha256:9f874730f3823c2791795017b7c8e0c3ca27a191adeeaed3164319202e6bbeb4"}, + {file = "ruff-0.0.217-py3-none-win_amd64.whl", hash = "sha256:56f4205976e33c02bd8af0eaa7e35be0e24d9945b7de56afebf735b26be2a67b"}, + {file = "ruff-0.0.217.tar.gz", hash = "sha256:39b2b1de9330fcf60643bdd6c4c660b457390c686b4ba7101bea019a01446494"}, +] +setuptools = [ + {file = "setuptools-66.1.1-py3-none-any.whl", hash = "sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b"}, + {file = "setuptools-66.1.1.tar.gz", hash = "sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8"}, +] +shellingham = [ + {file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"}, + {file = "shellingham-1.5.0.post1.tar.gz", hash = "sha256:823bc5fb5c34d60f285b624e7264f4dda254bc803a3774a147bf99c0e3004a28"}, +] +tabulate = [ + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, +] +termcolor = [ + {file = "termcolor-2.2.0-py3-none-any.whl", hash = "sha256:91ddd848e7251200eac969846cbae2dacd7d71c2871e92733289e7e3666f48e7"}, + {file = "termcolor-2.2.0.tar.gz", hash = "sha256:dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tomlkit = [ + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, +] +typeguard = [ + {file = "typeguard-2.13.3-py3-none-any.whl", hash = "sha256:5e3e3be01e887e7eafae5af63d1f36c849aaa94e3a0112097312aabfa16284f1"}, + {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, +] +typer = [ + {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"}, + {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"}, +] +types-python-dateutil = [ + {file = "types-python-dateutil-2.8.19.6.tar.gz", hash = "sha256:4a6f4cc19ce4ba1a08670871e297bf3802f55d4f129e6aa2443f540b6cf803d2"}, + {file = "types_python_dateutil-2.8.19.6-py3-none-any.whl", hash = "sha256:cfb7d31021c6bce6f3362c69af6e3abb48fe3e08854f02487e844ff910deec2a"}, +] +types-pyyaml = [ + {file = "types-PyYAML-6.0.12.3.tar.gz", hash = "sha256:17ce17b3ead8f06e416a3b1d5b8ddc6cb82a422bb200254dd8b469434b045ffc"}, + {file = "types_PyYAML-6.0.12.3-py3-none-any.whl", hash = "sha256:879700e9f215afb20ab5f849590418ab500989f83a57e635689e1d50ccc63f0c"}, +] +typing-extensions = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] +virtualenv = [ + {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, + {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, +] +vulture = [ + {file = "vulture-2.7-py2.py3-none-any.whl", hash = "sha256:bccc51064ed76db15a6b58277cea8885936af047f53d2655fb5de575e93d0bca"}, + {file = "vulture-2.7.tar.gz", hash = "sha256:67fb80a014ed9fdb599dd44bb96cb54311032a104106fc2e706ef7a6dad88032"}, +] +wcwidth = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] +win32-setctime = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5ae5f79 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,239 @@ +[build-system] + build-backend = "poetry.core.masonry.api" + requires = ["poetry-core>=1.0.0"] + +[tool.poetry] + authors = ["Nate Landau "] + description = "Make batch updates to Obsidian metadata" + homepage = "https://github.com/natelandau/obsidian-metadata" + keywords = ["obsidian"] + license = "GNU AFFERO" + name = "obsidian-metadata" + readme = "README.md" + repository = "https://github.com/natelandau/obsidian-metadata" + version = "0.0.0" + + [tool.poetry.scripts] # https://python-poetry.org/docs/pyproject/#scripts + obsidian-metadata = "obsidian_metadata.cli:app" + + [tool.poetry.dependencies] + loguru = "^0.6.0" + python = "^3.10" + questionary = "^1.10.0" + rich = "^13.2.0" + ruamel-yaml = "^0.17.21" + shellingham = "^1.4.0" + tomli = "^2.0.1" + typer = "^0.7.0" + + [tool.poetry.group.test.dependencies] + pytest = "^7.2.0" + pytest-clarity = "^1.0.1" + pytest-mock = "^3.10.0" + pytest-pretty-terminal = "^1.1.0" + pytest-xdist = "^3.1.0" + + [tool.poetry.group.dev.dependencies] + absolufy-imports = "^0.3.1" + black = "^22.12.0" + commitizen = "^2.39.1" + coverage = "^7.0.4" + interrogate = "^1.5.0" + mypy = "^0.991" + pdoc = "^12.3.1" + pep8-naming = "^0.13.3" + poethepoet = "^0.18.0" + pre-commit = "^2.21.0" + ruff = "^0.0.217" + typeguard = "^2.13.3" + types-python-dateutil = "^2.8.19.5" + types-pyyaml = "^6.0.12.2" + vulture = "^2.7" + +[tool.ruff] # https://github.com/charliermarsh/ruff + fix = true + ignore = [ + "B006", + "B008", + "D107", + "D203", + "D204", + "D213", + "D215", + "D400", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", + "E501", + "N805", + "PGH001", + "PGH003", + "UP007", + ] + ignore-init-module-imports = true + line-length = 100 + select = [ + "A", + "B", + "BLE", + "C4", + "C90", + "D", + "E", + "ERA", + "F", + "I", + "N", + "PGH", + "PLC", + "PLE", + "PLR", + "PLW", + "RET", + "RUF", + "SIM", + "TID", + "UP", + "W", + "YTT", + ] + src = ["src", "tests"] + target-version = "py310" + unfixable = ["ERA001", "F401", "F401", "UP007"] + +[tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report + exclude_lines = [ + 'def __repr__', + 'except [\w\s\._]+ as .*:', + 'log\.critical', + 'log\.debug', + 'log\.error', + 'log\.exception', + 'log\.info', + 'log\.success', + 'log\.trace', + 'log\.warning', + 'pragma: no cover', + 'raise Abort', + 'raise Exit', + 'raise typer\.Exit', + ] + fail_under = 50 + precision = 1 + show_missing = true + skip_covered = true + +[tool.coverage.run] + branch = true + command_line = "--module pytest" + data_file = "reports/.coverage" + source = ["src"] + +[tool.coverage.xml] + output = "reports/coverage.xml" + +[tool.black] + line-length = 100 + +[tool.commitizen] + bump_message = "bump(release): v$current_version → v$new_version" + tag_format = "v$version" + update_changelog_on_bump = true + version = "0.0.0" + version_files = [ + "pyproject.toml:version", + "src/obsidian_metadata/__version__.py:__version__", + ] + +[tool.interrogate] + exclude = ["build", "docs", "tests"] + fail-under = 90 + ignore-init-method = true + verbose = 2 + +[tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html + disallow_any_unimported = false + disallow_subclassing_any = false + disallow_untyped_decorators = false + disallow_untyped_defs = true + exclude = [ + 'tests/', # TOML literal string (single-quotes, regex okay, no escaping necessary) + ] + follow_imports = "normal" + ignore_missing_imports = true + junit_xml = "reports/mypy.xml" + no_implicit_optional = true + pretty = false + show_column_numbers = true + show_error_codes = true + show_error_context = true + strict_optional = false + warn_redundant_casts = true + warn_unreachable = true + warn_unused_ignores = true + +[tool.pytest.ini_options] + addopts = "--color=yes --doctest-modules --exitfirst --failed-first --strict-config --strict-markers --verbosity=2 --junitxml=reports/pytest.xml" + filterwarnings = ["error", "ignore::DeprecationWarning"] + testpaths = ["src", "tests"] + xfail_strict = true + +[tool.vulture] # https://pypi.org/project/vulture/ + # exclude = ["file*.py", "dir/"] + # ignore_decorators = ["@app.route", "@require_*"] + ignore_names = ["args", "cls", "indentless", "kwargs", "request", "version"] + # make_whitelist = true + min_confidence = 80 + paths = ["src", "tests"] + sort_by_size = true + verbose = false + +[tool.poe.tasks] + + [tool.poe.tasks.docs] + cmd = """ + pdoc + --docformat google + --output-directory docs + src/obsidian_metadata + """ + help = "Generate this package's docs" + + [tool.poe.tasks.lint] + help = "Lint this package" + + [[tool.poe.tasks.lint.sequence]] + shell = "ruff --extend-ignore=I001,D301 src/ tests/" + + [[tool.poe.tasks.lint.sequence]] + shell = "black --check src/ tests/" + + [[tool.poe.tasks.lint.sequence]] + shell = "poetry check" + + [[tool.poe.tasks.lint.sequence]] + shell = "mypy --config-file pyproject.toml src/" + + [[tool.poe.tasks.lint.sequence]] + shell = "vulture src/ tests/" + + [[tool.poe.tasks.lint.sequence]] + shell = "yamllint ." + + [[tool.poe.tasks.lint.sequence]] + shell = "interrogate -c pyproject.toml ." + +[tool.poe.tasks.test] + help = "Test this package" + + [[tool.poe.tasks.test.sequence]] + cmd = "coverage run" + + [[tool.poe.tasks.test.sequence]] + cmd = "coverage report" + + [[tool.poe.tasks.test.sequence]] + cmd = "coverage xml" diff --git a/scripts/pre-commit-hook.sh b/scripts/pre-commit-hook.sh new file mode 100755 index 0000000..126058e --- /dev/null +++ b/scripts/pre-commit-hook.sh @@ -0,0 +1,821 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2317 + +_mainScript_() { + + _customStopWords_() { + # DESC: Check if any specified stop words are in the commit diff. If found, the pre-commit hook will exit with a non-zero exit code. + # ARGS: + # $1 (Required): Path to file + # OUTS: + # 0: Success + # 1: Failure + # USAGE: + # _customStopWords_ "/path/to/file.sh" + # NOTE: + # Requires a plaintext stopword file located at + # `~/.git_stop_words` containing one stopword per line. + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local _gitDiffTmp + local FILE_TO_CHECK="${1}" + + _gitDiffTmp="${TMP_DIR}/${RANDOM}.${RANDOM}.${RANDOM}.diff.txt" + + if [ -f "${STOP_WORD_FILE}" ]; then + + if [[ $(basename "${STOP_WORD_FILE}") == "$(basename "${FILE_TO_CHECK}")" ]]; then + debug "$(basename "${1}"): Don't check stop words file for stop words." + return 0 + fi + debug "$(basename "${FILE_TO_CHECK}"): Checking for stop words..." + + # remove blank lines from stopwords file + sed '/^$/d' "${STOP_WORD_FILE}" >"${TMP_DIR}/pattern_file.txt" + + # Check for stopwords + if git diff --cached -- "${FILE_TO_CHECK}" | grep –i -q "new file mode"; then + if grep -i --file="${TMP_DIR}/pattern_file.txt" "${FILE_TO_CHECK}"; then + return 1 + else + return 0 + fi + else + # Add diff to a temporary file + git diff --cached -- "${FILE_TO_CHECK}" | grep '^+' >"${_gitDiffTmp}" + if grep -i --file="${TMP_DIR}/pattern_file.txt" "${_gitDiffTmp}"; then + return 1 + else + return 0 + fi + fi + + else + + notice "Could not find git stopwords file expected at '${STOP_WORD_FILE}'. Continuing..." + return 0 + fi + } + + # Don;t lint binary files + if [[ ${ARGS[0]} =~ \.(jpg|jpeg|gif|png|exe|zip|gzip|tiff|tar|dmg|ttf|otf|m4a|mp3|mkv|mov|avi|eot|svg|woff2?|aac|wav|flac|pdf|doc|xls|ppt|7z|bin|dmg|dat|sql|ico|mpe?g)$ ]]; then + _safeExit_ 0 + fi + + if ! _customStopWords_ "${ARGS[0]}"; then + error "Stop words found in ${ARGS[0]}" + _safeExit_ 1 + fi +} +# end _mainScript_ + +# ################################## Flags and defaults +# Required variables +LOGFILE="${HOME}/logs/$(basename "$0").log" +QUIET=false +LOGLEVEL=ERROR +VERBOSE=false +FORCE=false +DRYRUN=false +declare -a ARGS=() + +# Script specific +LOGLEVEL=NONE +STOP_WORD_FILE="${HOME}/.git_stop_words" +shopt -s nocasematch +# ################################## Custom utility functions (Pasted from repository) + +# ################################## Functions required for this template to work + +_setColors_() { + # DESC: + # Sets colors use for alerts. + # ARGS: + # None + # OUTS: + # None + # USAGE: + # printf "%s\n" "${blue}Some text${reset}" + + if tput setaf 1 >/dev/null 2>&1; then + bold=$(tput bold) + underline=$(tput smul) + reverse=$(tput rev) + reset=$(tput sgr0) + + if [[ $(tput colors) -ge 256 ]] >/dev/null 2>&1; then + white=$(tput setaf 231) + blue=$(tput setaf 38) + yellow=$(tput setaf 11) + green=$(tput setaf 82) + red=$(tput setaf 9) + purple=$(tput setaf 171) + gray=$(tput setaf 250) + else + white=$(tput setaf 7) + blue=$(tput setaf 38) + yellow=$(tput setaf 3) + green=$(tput setaf 2) + red=$(tput setaf 9) + purple=$(tput setaf 13) + gray=$(tput setaf 7) + fi + else + bold="\033[4;37m" + reset="\033[0m" + underline="\033[4;37m" + # shellcheck disable=SC2034 + reverse="" + white="\033[0;37m" + blue="\033[0;34m" + yellow="\033[0;33m" + green="\033[1;32m" + red="\033[0;31m" + purple="\033[0;35m" + gray="\033[0;37m" + fi +} + +_alert_() { + # DESC: + # Controls all printing of messages to log files and stdout. + # ARGS: + # $1 (required) - The type of alert to print + # (success, header, notice, dryrun, debug, warning, error, + # fatal, info, input) + # $2 (required) - The message to be printed to stdout and/or a log file + # $3 (optional) - Pass '${LINENO}' to print the line number where the _alert_ was triggered + # OUTS: + # stdout: The message is printed to stdout + # log file: The message is printed to a log file + # USAGE: + # [_alertType] "[MESSAGE]" "${LINENO}" + # NOTES: + # - The colors of each alert type are set in this function + # - For specified alert types, the funcstac will be printed + + local _color + local _alertType="${1}" + local _message="${2}" + local _line="${3-}" # Optional line number + + [[ $# -lt 2 ]] && fatal 'Missing required argument to _alert_' + + if [[ -n ${_line} && ${_alertType} =~ ^fatal && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}(line: ${_line}) $(_printFuncStack_)" + elif [[ -n ${_line} && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}(line: ${_line})" + elif [[ -z ${_line} && ${_alertType} =~ ^fatal && ${FUNCNAME[2]} != "_trapCleanup_" ]]; then + _message="${_message} ${gray}$(_printFuncStack_)" + fi + + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _color="${bold}${red}" + elif [ "${_alertType}" == "info" ]; then + _color="${gray}" + elif [ "${_alertType}" == "warning" ]; then + _color="${red}" + elif [ "${_alertType}" == "success" ]; then + _color="${green}" + elif [ "${_alertType}" == "debug" ]; then + _color="${purple}" + elif [ "${_alertType}" == "header" ]; then + _color="${bold}${white}${underline}" + elif [ "${_alertType}" == "notice" ]; then + _color="${bold}" + elif [ "${_alertType}" == "input" ]; then + _color="${bold}${underline}" + elif [ "${_alertType}" = "dryrun" ]; then + _color="${blue}" + else + _color="" + fi + + _writeToScreen_() { + ("${QUIET}") && return 0 # Print to console when script is not 'quiet' + [[ ${VERBOSE} == false && ${_alertType} =~ ^(debug|verbose) ]] && return 0 + + if ! [[ -t 1 || -z ${TERM-} ]]; then # Don't use colors on non-recognized terminals + _color="" + reset="" + fi + + if [[ ${_alertType} == header ]]; then + printf "${_color}%s${reset}\n" "${_message}" + else + printf "${_color}[%7s] %s${reset}\n" "${_alertType}" "${_message}" + fi + } + _writeToScreen_ + + _writeToLog_() { + [[ ${_alertType} == "input" ]] && return 0 + [[ ${LOGLEVEL} =~ (off|OFF|Off) ]] && return 0 + if [ -z "${LOGFILE-}" ]; then + LOGFILE="$(pwd)/$(basename "$0").log" + fi + [ ! -d "$(dirname "${LOGFILE}")" ] && mkdir -p "$(dirname "${LOGFILE}")" + [[ ! -f ${LOGFILE} ]] && touch "${LOGFILE}" + + # Don't use colors in logs + local _cleanmessage + _cleanmessage="$(printf "%s" "${_message}" | sed -E 's/(\x1b)?\[(([0-9]{1,2})(;[0-9]{1,3}){0,2})?[mGK]//g')" + # Print message to log file + printf "%s [%7s] %s %s\n" "$(date +"%b %d %R:%S")" "${_alertType}" "[$(/bin/hostname)]" "${_cleanmessage}" >>"${LOGFILE}" + } + + # Write specified log level data to logfile + case "${LOGLEVEL:-ERROR}" in + ALL | all | All) + _writeToLog_ + ;; + DEBUG | debug | Debug) + _writeToLog_ + ;; + INFO | info | Info) + if [[ ${_alertType} =~ ^(error|fatal|warning|info|notice|success) ]]; then + _writeToLog_ + fi + ;; + NOTICE | notice | Notice) + if [[ ${_alertType} =~ ^(error|fatal|warning|notice|success) ]]; then + _writeToLog_ + fi + ;; + WARN | warn | Warn) + if [[ ${_alertType} =~ ^(error|fatal|warning) ]]; then + _writeToLog_ + fi + ;; + ERROR | error | Error) + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _writeToLog_ + fi + ;; + FATAL | fatal | Fatal) + if [[ ${_alertType} =~ ^fatal ]]; then + _writeToLog_ + fi + ;; + OFF | off) + return 0 + ;; + *) + if [[ ${_alertType} =~ ^(error|fatal) ]]; then + _writeToLog_ + fi + ;; + esac + +} # /_alert_ + +error() { _alert_ error "${1}" "${2-}"; } +warning() { _alert_ warning "${1}" "${2-}"; } +notice() { _alert_ notice "${1}" "${2-}"; } +info() { _alert_ info "${1}" "${2-}"; } +success() { _alert_ success "${1}" "${2-}"; } +dryrun() { _alert_ dryrun "${1}" "${2-}"; } +input() { _alert_ input "${1}" "${2-}"; } +header() { _alert_ header "${1}" "${2-}"; } +debug() { _alert_ debug "${1}" "${2-}"; } +fatal() { + _alert_ fatal "${1}" "${2-}" + _safeExit_ "1" +} + +_printFuncStack_() { + # DESC: + # Prints the function stack in use. Used for debugging, and error reporting. + # ARGS: + # None + # OUTS: + # stdout: Prints [function]:[file]:[line] + # NOTE: + # Does not print functions from the alert class + local _i + declare -a _funcStackResponse=() + for ((_i = 1; _i < ${#BASH_SOURCE[@]}; _i++)); do + case "${FUNCNAME[${_i}]}" in + _alert_ | _trapCleanup_ | fatal | error | warning | notice | info | debug | dryrun | header | success) + continue + ;; + *) + _funcStackResponse+=("${FUNCNAME[${_i}]}:$(basename "${BASH_SOURCE[${_i}]}"):${BASH_LINENO[_i - 1]}") + ;; + esac + + done + printf "( " + printf %s "${_funcStackResponse[0]}" + printf ' < %s' "${_funcStackResponse[@]:1}" + printf ' )\n' +} + +_safeExit_() { + # DESC: + # Cleanup and exit from a script + # ARGS: + # $1 (optional) - Exit code (defaults to 0) + # OUTS: + # None + + if [[ -d ${SCRIPT_LOCK-} ]]; then + if command rm -rf "${SCRIPT_LOCK}"; then + debug "Removing script lock" + else + warning "Script lock could not be removed. Try manually deleting ${yellow}'${SCRIPT_LOCK}'" + fi + fi + + if [[ -n ${TMP_DIR-} && -d ${TMP_DIR-} ]]; then + if [[ ${1-} == 1 && -n "$(ls "${TMP_DIR}")" ]]; then + command rm -r "${TMP_DIR}" + else + command rm -r "${TMP_DIR}" + debug "Removing temp directory" + fi + fi + + trap - INT TERM EXIT + exit "${1:-0}" +} + +_trapCleanup_() { + # DESC: + # Log errors and cleanup from script when an error is trapped. Called by 'trap' + # ARGS: + # $1: Line number where error was trapped + # $2: Line number in function + # $3: Command executing at the time of the trap + # $4: Names of all shell functions currently in the execution call stack + # $5: Scriptname + # $6: $BASH_SOURCE + # USAGE: + # trap '_trapCleanup_ ${LINENO} ${BASH_LINENO} "${BASH_COMMAND}" "${FUNCNAME[*]}" "${0}" "${BASH_SOURCE[0]}"' EXIT INT TERM SIGINT SIGQUIT SIGTERM ERR + # OUTS: + # Exits script with error code 1 + + local _line=${1-} # LINENO + local _linecallfunc=${2-} + local _command="${3-}" + local _funcstack="${4-}" + local _script="${5-}" + local _sourced="${6-}" + + # Replace the cursor in-case 'tput civis' has been used + tput cnorm + + if declare -f "fatal" &>/dev/null && declare -f "_printFuncStack_" &>/dev/null; then + + _funcstack="'$(printf "%s" "${_funcstack}" | sed -E 's/ / < /g')'" + + if [[ ${_script##*/} == "${_sourced##*/}" ]]; then + fatal "${7-} command: '${_command}' (line: ${_line}) [func: $(_printFuncStack_)]" + else + fatal "${7-} command: '${_command}' (func: ${_funcstack} called at line ${_linecallfunc} of '${_script##*/}') (line: ${_line} of '${_sourced##*/}') " + fi + else + printf "%s\n" "Fatal error trapped. Exiting..." + fi + + if declare -f _safeExit_ &>/dev/null; then + _safeExit_ 1 + else + exit 1 + fi +} + +_makeTempDir_() { + # DESC: + # Creates a temp directory to house temporary files + # ARGS: + # $1 (Optional) - First characters/word of directory name + # OUTS: + # Sets $TMP_DIR variable to the path of the temp directory + # USAGE: + # _makeTempDir_ "$(basename "$0")" + + [ -d "${TMP_DIR-}" ] && return 0 + + if [ -n "${1-}" ]; then + TMP_DIR="${TMPDIR:-/tmp/}${1}.${RANDOM}.${RANDOM}.$$" + else + TMP_DIR="${TMPDIR:-/tmp/}$(basename "$0").${RANDOM}.${RANDOM}.${RANDOM}.$$" + fi + (umask 077 && mkdir "${TMP_DIR}") || { + fatal "Could not create temporary directory! Exiting." + } + debug "\$TMP_DIR=${TMP_DIR}" +} + +# shellcheck disable=SC2120 +_acquireScriptLock_() { + # DESC: + # Acquire script lock to prevent running the same script a second time before the + # first instance exits + # ARGS: + # $1 (optional) - Scope of script execution lock (system or user) + # OUTS: + # exports $SCRIPT_LOCK - Path to the directory indicating we have the script lock + # Exits script if lock cannot be acquired + # NOTE: + # If the lock was acquired it's automatically released in _safeExit_() + + local _lockDir + if [[ ${1-} == 'system' ]]; then + _lockDir="${TMPDIR:-/tmp/}$(basename "$0").lock" + else + _lockDir="${TMPDIR:-/tmp/}$(basename "$0").${UID}.lock" + fi + + if command mkdir "${_lockDir}" 2>/dev/null; then + readonly SCRIPT_LOCK="${_lockDir}" + debug "Acquired script lock: ${yellow}${SCRIPT_LOCK}${purple}" + else + if declare -f "_safeExit_" &>/dev/null; then + error "Unable to acquire script lock: ${yellow}${_lockDir}${red}" + fatal "If you trust the script isn't running, delete the lock dir" + else + printf "%s\n" "ERROR: Could not acquire script lock. If you trust the script isn't running, delete: ${_lockDir}" + exit 1 + fi + + fi +} + +_setPATH_() { + # DESC: + # Add directories to $PATH so script can find executables + # ARGS: + # $@ - One or more paths + # OPTS: + # -x - Fail if directories are not found + # OUTS: + # 0: Success + # 1: Failure + # Adds items to $PATH + # USAGE: + # _setPATH_ "/usr/local/bin" "${HOME}/bin" "$(npm bin)" + + [[ $# == 0 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local opt + local OPTIND=1 + local _failIfNotFound=false + + while getopts ":xX" opt; do + case ${opt} in + x | X) _failIfNotFound=true ;; + *) + { + error "Unrecognized option '${1}' passed to _backupFile_" "${LINENO}" + return 1 + } + ;; + esac + done + shift $((OPTIND - 1)) + + local _newPath + + for _newPath in "$@"; do + if [ -d "${_newPath}" ]; then + if ! printf "%s" "${PATH}" | grep -Eq "(^|:)${_newPath}($|:)"; then + if PATH="${_newPath}:${PATH}"; then + debug "Added '${_newPath}' to PATH" + else + debug "'${_newPath}' already in PATH" + fi + else + debug "_setPATH_: '${_newPath}' already exists in PATH" + fi + else + debug "_setPATH_: can not find: ${_newPath}" + if [[ ${_failIfNotFound} == true ]]; then + return 1 + fi + continue + fi + done + return 0 +} + +_useGNUutils_() { + # DESC: + # Add GNU utilities to PATH to allow consistent use of sed/grep/tar/etc. on MacOS + # ARGS: + # None + # OUTS: + # 0 if successful + # 1 if unsuccessful + # PATH: Adds GNU utilities to the path + # USAGE: + # # if ! _useGNUUtils_; then exit 1; fi + # NOTES: + # GNU utilities can be added to MacOS using Homebrew + + ! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_" + + if _setPATH_ \ + "/usr/local/opt/gnu-tar/libexec/gnubin" \ + "/usr/local/opt/coreutils/libexec/gnubin" \ + "/usr/local/opt/gnu-sed/libexec/gnubin" \ + "/usr/local/opt/grep/libexec/gnubin" \ + "/usr/local/opt/findutils/libexec/gnubin" \ + "/opt/homebrew/opt/findutils/libexec/gnubin" \ + "/opt/homebrew/opt/gnu-sed/libexec/gnubin" \ + "/opt/homebrew/opt/grep/libexec/gnubin" \ + "/opt/homebrew/opt/coreutils/libexec/gnubin" \ + "/opt/homebrew/opt/gnu-tar/libexec/gnubin"; then + return 0 + else + return 1 + fi + +} + +_homebrewPath_() { + # DESC: + # Add homebrew bin dir to PATH + # ARGS: + # None + # OUTS: + # 0 if successful + # 1 if unsuccessful + # PATH: Adds homebrew bin directory to PATH + # USAGE: + # # if ! _homebrewPath_; then exit 1; fi + + ! declare -f "_setPATH_" &>/dev/null && fatal "${FUNCNAME[0]} needs function _setPATH_" + + if _uname=$(command -v uname); then + if "${_uname}" | tr '[:upper:]' '[:lower:]' | grep -q 'darwin'; then + if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then + return 0 + else + return 1 + fi + fi + else + if _setPATH_ "/usr/local/bin" "/opt/homebrew/bin"; then + return 0 + else + return 1 + fi + fi +} + +_parseOptions_() { + # DESC: + # Iterates through options passed to script and sets variables. Will break -ab into -a -b + # when needed and --foo=bar into --foo bar + # ARGS: + # $@ from command line + # OUTS: + # Sets array 'ARGS' containing all arguments passed to script that were not parsed as options + # USAGE: + # _parseOptions_ "$@" + + # Iterate over options + local _optstring=h + declare -a _options + local _c + local i + while (($#)); do + case $1 in + # If option is of type -ab + -[!-]?*) + # Loop over each character starting with the second + for ((i = 1; i < ${#1}; i++)); do + _c=${1:i:1} + _options+=("-${_c}") # Add current char to options + # If option takes a required argument, and it's not the last char make + # the rest of the string its argument + if [[ ${_optstring} == *"${_c}:"* && -n ${1:i+1} ]]; then + _options+=("${1:i+1}") + break + fi + done + ;; + # If option is of type --foo=bar + --?*=*) _options+=("${1%%=*}" "${1#*=}") ;; + # add --endopts for -- + --) _options+=(--endopts) ;; + # Otherwise, nothing special + *) _options+=("$1") ;; + esac + shift + done + set -- "${_options[@]-}" + unset _options + + # Read the options and set stuff + # shellcheck disable=SC2034 + while [[ ${1-} == -?* ]]; do + case $1 in + # Custom options + + # Common options + -h | --help) + _usage_ + _safeExit_ + ;; + --loglevel) + shift + LOGLEVEL=${1} + ;; + --logfile) + shift + LOGFILE="${1}" + ;; + -n | --dryrun) DRYRUN=true ;; + -v | --verbose) VERBOSE=true ;; + -q | --quiet) QUIET=true ;; + --force) FORCE=true ;; + --endopts) + shift + break + ;; + *) + if declare -f _safeExit_ &>/dev/null; then + fatal "invalid option: $1" + else + printf "%s\n" "ERROR: Invalid option: $1" + exit 1 + fi + ;; + esac + shift + done + + if [[ -z ${*} || ${*} == null ]]; then + ARGS=() + else + ARGS+=("$@") # Store the remaining user input as arguments. + fi +} + +_columns_() { + # DESC: + # Prints a two column output from a key/value pair. + # Optionally pass a number of 2 space tabs to indent the output. + # ARGS: + # $1 (required): Key name (Left column text) + # $2 (required): Long value (Right column text. Wraps around if too long) + # $3 (optional): Number of 2 character tabs to indent the command (default 1) + # OPTS: + # -b Bold the left column + # -u Underline the left column + # -r Reverse background and foreground colors + # OUTS: + # stdout: Prints the output in columns + # NOTE: + # Long text or ANSI colors in the first column may create display issues + # USAGE: + # _columns_ "Key" "Long value text" [tab level] + + [[ $# -lt 2 ]] && fatal "Missing required argument to ${FUNCNAME[0]}" + + local opt + local OPTIND=1 + local _style="" + while getopts ":bBuUrR" opt; do + case ${opt} in + b | B) _style="${_style}${bold}" ;; + u | U) _style="${_style}${underline}" ;; + r | R) _style="${_style}${reverse}" ;; + *) fatal "Unrecognized option '${1}' passed to ${FUNCNAME[0]}. Exiting." ;; + esac + done + shift $((OPTIND - 1)) + + local _key="${1}" + local _value="${2}" + local _tabLevel="${3-}" + local _tabSize=2 + local _line + local _rightIndent + local _leftIndent + if [[ -z ${3-} ]]; then + _tabLevel=0 + fi + + _leftIndent="$((_tabLevel * _tabSize))" + + local _leftColumnWidth="$((30 + _leftIndent))" + + if [ "$(tput cols)" -gt 180 ]; then + _rightIndent=110 + elif [ "$(tput cols)" -gt 160 ]; then + _rightIndent=90 + elif [ "$(tput cols)" -gt 130 ]; then + _rightIndent=60 + elif [ "$(tput cols)" -gt 120 ]; then + _rightIndent=50 + elif [ "$(tput cols)" -gt 110 ]; then + _rightIndent=40 + elif [ "$(tput cols)" -gt 100 ]; then + _rightIndent=30 + elif [ "$(tput cols)" -gt 90 ]; then + _rightIndent=20 + elif [ "$(tput cols)" -gt 80 ]; then + _rightIndent=10 + else + _rightIndent=0 + fi + + local _rightWrapLength=$(($(tput cols) - _leftColumnWidth - _leftIndent - _rightIndent)) + + local _first_line=0 + while read -r _line; do + if [[ ${_first_line} -eq 0 ]]; then + _first_line=1 + else + _key=" " + fi + printf "%-${_leftIndent}s${_style}%-${_leftColumnWidth}b${reset} %b\n" "" "${_key}${reset}" "${_line}" + done <<<"$(fold -w${_rightWrapLength} -s <<<"${_value}")" +} + +_usage_() { + cat < None: + self.config_path: Path = self._validate_config_path(Path(config_path)) + self.config: dict[str, Any] = self._load_config() + self.config_content: str = self.config_path.read_text() + self.vault_path: Path = self._validate_vault_path(vault_path) + + try: + self.exclude_paths: list[Any] = self.config["exclude_paths"] + except KeyError: + self.exclude_paths = [] + + try: + self.metadata_location: str = self.config["metadata"]["metadata_location"] + except KeyError: + self.metadata_location = "frontmatter" + + try: + self.tags_location: str = self.config["metadata"]["tags_location"] + except KeyError: + self.tags_location = "top" + + log.debug(f"Loaded configuration from '{self.config_path}'") + log.trace(self.config) + + def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover + """Define rich representation of Vault.""" + yield "config_path", self.config_path + yield "config_content", + yield "vault_path", self.vault_path + yield "metadata_location", self.metadata_location + yield "tags_location", self.tags_location + yield "exclude_paths", self.exclude_paths + + def _validate_config_path(self, config_path: Path | None) -> Path: + """Load the configuration path.""" + if config_path is None: + config_path = Path(Path.home() / f".{__package__.split('.')[0]}.toml") + + if not config_path.exists(): + shutil.copy(DEFAULT_CONFIG_FILE, config_path) + alerts.info(f"Created default configuration file at '{config_path}'") + + return config_path.expanduser().resolve() + + def _load_config(self) -> dict[str, Any]: + """Load the configuration file.""" + try: + with self.config_path.open("rb") as f: + return tomllib.load(f) + except tomllib.TOMLDecodeError as e: + alerts.error(f"Could not parse '{self.config_path}'") + raise typer.Exit(code=1) from e + + def _validate_vault_path(self, vault_path: Path | None) -> Path: + """Validate the vault path.""" + if vault_path is None: + try: + vault_path = Path(self.config["vault"]).expanduser().resolve() + except KeyError: + vault_path = Path("/I/Do/Not/Exist") + + if not vault_path.exists(): # pragma: no cover + alerts.error(f"Vault path not found: '{vault_path}'") + + vault_path = questionary.path( + "Enter a path to Obsidian vault:", + only_directories=True, + validate=vault_validation, + ).ask() + if vault_path is None: + raise typer.Exit(code=1) + + vault_path = Path(vault_path).expanduser().resolve() + + self.write_config_value("vault", str(vault_path)) + return vault_path + + def write_config_value(self, key: str, value: str | int) -> None: + """Write a new value to the configuration file. + + Args: + key (str): The key to write. + value (str|int): The value to write. + """ + self.config_content = re.sub( + rf"( *{key} = ['\"])[^'\"]*(['\"].*)", rf"\1{value}\2", self.config_content + ) + + alerts.notice(f"Writing new configuration for '{key}' to '{self.config_path}'") + self.config_path.write_text(self.config_content) diff --git a/src/obsidian_metadata/_config/default.toml b/src/obsidian_metadata/_config/default.toml new file mode 100644 index 0000000..a6f94da --- /dev/null +++ b/src/obsidian_metadata/_config/default.toml @@ -0,0 +1,5 @@ +# Path to your obsidian vault +vault = "/path/to/vault" + +# Folders within the vault to ignore when indexing metadata +exclude_paths = [".git", ".obsidian"] diff --git a/src/obsidian_metadata/_utils/__init__.py b/src/obsidian_metadata/_utils/__init__.py new file mode 100644 index 0000000..b00ef24 --- /dev/null +++ b/src/obsidian_metadata/_utils/__init__.py @@ -0,0 +1,27 @@ +"""Shared utilities.""" + +from obsidian_metadata._utils import alerts +from obsidian_metadata._utils.alerts import LoggerManager +from obsidian_metadata._utils.utilities import ( + clean_dictionary, + clear_screen, + dict_contains, + dict_values_to_lists_strings, + docstring_parameter, + remove_markdown_sections, + vault_validation, + version_callback, +) + +__all__ = [ + "alerts", + "clean_dictionary", + "clear_screen", + "dict_values_to_lists_strings", + "dict_contains", + "docstring_parameter", + "LoggerManager", + "remove_markdown_sections", + "vault_validation", + "version_callback", +] diff --git a/src/obsidian_metadata/_utils/alerts.py b/src/obsidian_metadata/_utils/alerts.py new file mode 100644 index 0000000..6e8a868 --- /dev/null +++ b/src/obsidian_metadata/_utils/alerts.py @@ -0,0 +1,242 @@ +"""Logging and alerts.""" +import sys +from pathlib import Path + +import rich.repr +import typer +from loguru import logger +from rich import print + + +def dryrun(msg: str) -> None: + """Print a message if the dry run flag is set. + + Args: + msg: Message to print + """ + print(f"[cyan]DRYRUN | {msg}[/cyan]") + + +def success(msg: str) -> None: + """Print a success message without using logging. + + Args: + msg: Message to print + """ + print(f"[green]SUCCESS | {msg}[/green]") + + +def warning(msg: str) -> None: + """Print a warning message without using logging. + + Args: + msg: Message to print + """ + print(f"[yellow]WARNING | {msg}[/yellow]") + + +def error(msg: str) -> None: + """Print an error message without using logging. + + Args: + msg: Message to print + """ + print(f"[red]ERROR | {msg}[/red]") + + +def notice(msg: str) -> None: + """Print a notice message without using logging. + + Args: + msg: Message to print + """ + print(f"[bold]NOTICE | {msg}[/bold]") + + +def info(msg: str) -> None: + """Print a notice message without using logging. + + Args: + msg: Message to print + """ + print(f"INFO | {msg}") + + +def dim(msg: str) -> None: + """Print a message in dimmed color. + + Args: + msg: Message to print + """ + print(f"[dim]{msg}[/dim]") + + +def _log_formatter(record: dict) -> str: + """Create custom log formatter based on the log level. This effects the logs sent to stdout/stderr but not the log file.""" + if ( + record["level"].name == "INFO" + or record["level"].name == "SUCCESS" + or record["level"].name == "WARNING" + ): + return "{level: <8} | {message}\n{exception}" + + return "{level: <8} | {message} ({name}:{function}:{line})\n{exception}" + + +@rich.repr.auto +class LoggerManager: + """Instantiate the loguru logging system with the following levels. + + - TRACE: Usage: log.trace("") + - DEBUG: Usage: log.debug("") + - INFO: Usage: log.info("") + - WARNING: Usage: log.warning("") + - ERROR: Usage: log.error("") + - CRITICAL: Usage: log.critical("") + - EXCEPTION: Usage: log.exception("") + + Attributes: + log_file (Path): Path to the log file. + verbosity (int): Verbosity level. + log_to_file (bool): Whether to log to a file. + log_level (int): Default log level (verbosity overrides this) + + Examples: + Instantiate the logger: + + logging = _utils.alerts.LoggerManager( + verbosity, + log_to_file, + log_file, + log_level) + """ + + def __init__( + self, + log_file: Path = Path("/logs"), + verbosity: int = 0, + log_to_file: bool = False, + log_level: int = 30, + ) -> None: + self.verbosity = verbosity + self.log_to_file = log_to_file + self.log_file = log_file + self.log_level = log_level + + if self.log_file == Path("/logs") and self.log_to_file: # pragma: no cover + print("No log file specified") + raise typer.Exit(1) + + if self.verbosity >= 3: + logger.remove() + logger.add( + sys.stderr, + level="TRACE", + format=_log_formatter, # type: ignore[arg-type] + backtrace=False, + diagnose=True, + ) + self.log_level = 5 + elif self.verbosity == 2: + logger.remove() + logger.add( + sys.stderr, + level="DEBUG", + format=_log_formatter, # type: ignore[arg-type] + backtrace=False, + diagnose=True, + ) + self.log_level = 10 + elif self.verbosity == 1: + logger.remove() + logger.add( + sys.stderr, + level="INFO", + format=_log_formatter, # type: ignore[arg-type] + backtrace=False, + diagnose=True, + ) + self.log_level = 20 + else: + logger.remove() + logger.add( + sys.stderr, + format=_log_formatter, # type: ignore[arg-type] + level="WARNING", + backtrace=False, + diagnose=True, + ) + self.log_level = 30 + + if self.log_to_file is True: + logger.add( + self.log_file, + rotation="5 MB", + level=self.log_level, + backtrace=False, + diagnose=True, + delay=True, + ) + logger.debug(f"Logging to file: {self.log_file}") + + logger.debug("Logging instantiated") + + def is_trace(self, msg: str | None = None) -> bool: + """Check if the current log level is TRACE. + + Args: + msg (optional): Message to print. Defaults to None. + + Returns: + bool: True if the current log level is TRACE or lower, False otherwise. + """ + if self.log_level <= 5: + if msg: + print(msg) + return True + return False + + def is_debug(self, msg: str | None = None) -> bool: + """Check if the current log level is DEBUG. + + Args: + msg (optional): Message to print. Defaults to None. + + Returns: + bool: True if the current log level is DEBUG or lower, False otherwise. + """ + if self.log_level <= 10: + if msg: + print(msg) + return True + return False + + def is_info(self, msg: str | None = None) -> bool: + """Check if the current log level is INFO. + + Args: + msg (optional): Message to print. Defaults to None. + + Returns: + bool: True if the current log level is INFO or lower, False otherwise. + """ + if self.log_level <= 20: + if msg: + print(msg) + return True + return False + + def is_default(self, msg: str | None = None) -> bool: + """Check if the current log level is default level (SUCCESS or WARNING). + + Args: + msg (optional): Message to print. Defaults to None. + + Returns: + bool: True if the current log level is default or lower, False otherwise. + """ + if self.log_level <= 30: + if msg: + print(msg) + return True + return False # pragma: no cover diff --git a/src/obsidian_metadata/_utils/utilities.py b/src/obsidian_metadata/_utils/utilities.py new file mode 100644 index 0000000..ce7cb8d --- /dev/null +++ b/src/obsidian_metadata/_utils/utilities.py @@ -0,0 +1,169 @@ +"""Utility functions.""" +import re +from os import name, system +from pathlib import Path +from typing import Any + +import typer + +from obsidian_metadata.__version__ import __version__ + + +def dict_values_to_lists_strings(dictionary: dict, strip_null_values: bool = False) -> dict: + """Converts all values in a dictionary to lists of strings. + + Args: + dictionary (dict): Dictionary to convert + strip_null (bool): Whether to strip null values + + Returns: + dict: Dictionary with all values converted to lists of strings + + {key: sorted(new_dict[key]) for key in sorted(new_dict)} + """ + new_dict = {} + + if strip_null_values: + for key, value in dictionary.items(): + if isinstance(value, list): + new_dict[key] = sorted([str(item) for item in value if item is not None]) + elif isinstance(value, dict): + new_dict[key] = dict_values_to_lists_strings(value) # type: ignore[assignment] + elif value is None or value == "None" or value == "": + new_dict[key] = [] + else: + new_dict[key] = [str(value)] + + return new_dict + + for key, value in dictionary.items(): + if isinstance(value, list): + new_dict[key] = sorted([str(item) for item in value]) + elif isinstance(value, dict): + new_dict[key] = dict_values_to_lists_strings(value) # type: ignore[assignment] + else: + new_dict[key] = [str(value)] + + return new_dict + + +def remove_markdown_sections( + text: str, + strip_codeblocks: bool = False, + strip_inlinecode: bool = False, + strip_frontmatter: bool = False, +) -> str: + """Strips markdown sections from text. + + Args: + text (str): Text to remove code blocks from + strip_codeblocks (bool, optional): Strip code blocks. Defaults to False. + strip_inlinecode (bool, optional): Strip inline code. Defaults to False. + strip_frontmatter (bool, optional): Strip frontmatter. Defaults to False. + + Returns: + str: Text without code blocks + """ + if strip_codeblocks: + text = re.sub(r"`{3}.*?`{3}", "", text, flags=re.DOTALL) + + if strip_inlinecode: + text = re.sub(r"`.*?`", "", text) + + if strip_frontmatter: + text = re.sub(r"^\s*---.*?---", "", text, flags=re.DOTALL) + + return text # noqa: RET504 + + +def version_callback(value: bool) -> None: + """Print version and exit.""" + if value: + print(f"{__package__.split('.')[0]}: v{__version__}") + raise typer.Exit() + + +def vault_validation(path: str) -> bool | str: + """Validates the vault path.""" + path_to_validate: Path = Path(path).expanduser().resolve() + if not path_to_validate.exists(): + return f"Path does not exist: {path_to_validate}" + if not path_to_validate.is_dir(): + return f"Path is not a directory: {path_to_validate}" + + return True + + +def docstring_parameter(*sub: Any) -> Any: + """Decorator to replace variables within docstrings. + + Args: + sub (Any): Replacement variables + + Usage: + @docstring_parameter("foo", "bar") + def foo(): + '''This is a {0} docstring with {1} variables.''' + + """ + + def dec(obj: Any) -> Any: + """Format object.""" + obj.__doc__ = obj.__doc__.format(*sub) + return obj + + return dec + + +def clean_dictionary(dictionary: dict[str, Any]) -> dict[str, Any]: + """Clean up a dictionary by markdown formatting from keys and values. + + Args: + dictionary (dict): Dictionary to clean + + Returns: + dict: Cleaned dictionary + """ + new_dict = {key.strip(): value for key, value in dictionary.items()} + new_dict = {key.strip("*[]#"): value for key, value in new_dict.items()} + for key, value in new_dict.items(): + new_dict[key] = [s.strip("*[]#") for s in value if isinstance(value, list)] + + return new_dict + + +def clear_screen() -> None: + """Clears the screen.""" + # for windows + _ = system("cls") if name == "nt" else system("clear") + + +def dict_contains( + dictionary: dict[str, list[str]], key: str, value: str = None, is_regex: bool = False +) -> bool: + """Checks if a dictionary contains a key. + + Args: + dictionary (dict): Dictionary to check + key (str): Key to check for + value (str, optional): Value to check for. Defaults to None. + is_regex (bool, optional): Whether the key is a regex. Defaults to False. + + Returns: + bool: Whether the dictionary contains the key + """ + if value is None: + if is_regex: + return any(re.search(key, str(_key)) for _key in dictionary) + return key in dictionary + + if is_regex: + found_keys = [] + for _key in dictionary: + if re.search(key, str(_key)): + found_keys.append( + any(re.search(value, _v) for _v in dictionary[_key]), + ) + return any(found_keys) + + return key in dictionary and value in dictionary[key] diff --git a/src/obsidian_metadata/cli.py b/src/obsidian_metadata/cli.py new file mode 100644 index 0000000..b6efad4 --- /dev/null +++ b/src/obsidian_metadata/cli.py @@ -0,0 +1,115 @@ +"""obsidian-metadata CLI.""" + + +from pathlib import Path +from typing import Optional + +import typer +from rich import print + +from obsidian_metadata._config import Config +from obsidian_metadata._utils import alerts, docstring_parameter, version_callback +from obsidian_metadata.models import Application + +app = typer.Typer(add_completion=False, no_args_is_help=True, rich_markup_mode="rich") + +typer.rich_utils.STYLE_HELPTEXT = "" + +HELP_TEXT = """ +""" + + +@app.command() +@docstring_parameter(__package__) +def main( + vault_path: Path = typer.Option( + None, + help="Path to Obsidian vault", + show_default=False, + ), + config_file: Path = typer.Option( + Path(Path.home() / f".{__package__}.toml"), + help="Specify a custom path to a configuration file", + show_default=False, + ), + dry_run: bool = typer.Option( + False, + "--dry-run", + "-n", + help="Dry run - don't actually change anything", + ), + log_file: Path = typer.Option( + Path(Path.home() / "logs" / "obsidian_metadata.log"), + help="Path to log file", + show_default=True, + dir_okay=False, + file_okay=True, + exists=False, + ), + log_to_file: bool = typer.Option( + False, + "--log-to-file", + help="Log to file", + show_default=True, + ), + verbosity: int = typer.Option( + 0, + "-v", + "--verbose", + show_default=False, + help="""Set verbosity level (0=WARN, 1=INFO, 2=DEBUG, 3=TRACE)""", + count=True, + ), + version: Optional[bool] = typer.Option( + None, "--version", help="Print version and exit", callback=version_callback, is_eager=True + ), +) -> None: + r"""A script to make batch updates to metadata in an Obsidian vault. + + [bold] [/] + [bold underline]Features:[/] + + - [code]in-text tags:[/] delete every occurrence + - [code]in-text tags:[/] Rename tag ([dim]#tag1[/] -> [dim]#tag2[/]) + - [code]frontmatter:[/] Delete a key matching a regex pattern and all associated values + - [code]frontmatter:[/] Rename a key + - [code]frontmatter:[/] Delete a value matching a regex pattern from a specified key + - [code]frontmatter:[/] Rename a value from a specified key + - [code]inline metadata:[/] Delete a key matching a regex pattern and all associated values + - [code]inline metadata:[/] Rename a key + - [code]inline metadata:[/] Delete a value matching a regex pattern from a specified key + - [code]inline metadata:[/] Rename a value from a specified key + - [code]vault:[/] Create a backup of the Obsidian vault. + + [bold underline]Usage:[/] + Run [tan]obsidian-metadata[/] from the command line. The script will allow you to make batch updates to metadata in an Obsidian vault. Once you have made your changes, review them prior to committing them to the vault. + + Configuration is specified in a configuration file. On First run, this file will be created at [tan]~/.{0}.env[/]. Any options specified on the command line will override the configuration file. + """ + # Instantiate logger + alerts.LoggerManager( # pragma: no cover + log_file, + verbosity, + log_to_file, + ) + + config: Config = Config(config_path=config_file, vault_path=vault_path) + application = Application(dry_run=dry_run, config=config) + + banner = r""" + ___ _ _ _ _ + / _ \| |__ ___(_) __| (_) __ _ _ __ + | | | | '_ \/ __| |/ _` | |/ _` | '_ \ + | |_| | |_) \__ \ | (_| | | (_| | | | | + \___/|_.__/|___/_|\__,_|_|\__,_|_| |_| + | \/ | ___| |_ __ _ __| | __ _| |_ __ _ + | |\/| |/ _ \ __/ _` |/ _` |/ _` | __/ _` | + | | | | __/ || (_| | (_| | (_| | || (_| | + |_| |_|\___|\__\__,_|\__,_|\__,_|\__\__,_| +""" + print(banner) + application.main_app() + + +if __name__ == "__main__": + app() diff --git a/src/obsidian_metadata/models/__init__.py b/src/obsidian_metadata/models/__init__.py new file mode 100644 index 0000000..d4a0f44 --- /dev/null +++ b/src/obsidian_metadata/models/__init__.py @@ -0,0 +1,24 @@ +"""Shared models.""" +from obsidian_metadata.models.patterns import Patterns # isort: skip +from obsidian_metadata.models.metadata import ( + Frontmatter, + InlineMetadata, + InlineTags, + VaultMetadata, +) +from obsidian_metadata.models.notes import Note +from obsidian_metadata.models.vault import Vault + +from obsidian_metadata.models.application import Application # isort: skip + +__all__ = [ + "Frontmatter", + "InlineMetadata", + "InlineTags", + "LoggerManager", + "Note", + "Patterns", + "Application", + "Vault", + "VaultMetadata", +] diff --git a/src/obsidian_metadata/models/application.py b/src/obsidian_metadata/models/application.py new file mode 100644 index 0000000..b13746e --- /dev/null +++ b/src/obsidian_metadata/models/application.py @@ -0,0 +1,370 @@ +"""Questions for the cli.""" + + +from typing import Any + +import questionary +import typer +from rich import print + +from obsidian_metadata._config import Config +from obsidian_metadata._utils import alerts, clear_screen +from obsidian_metadata._utils.alerts import logger as log +from obsidian_metadata.models import Patterns, Vault + +PATTERNS = Patterns() + + +class Application: + """Questions for use in the cli. + + Contains methods which ask a series of questions to the user and return a dictionary with their answers. + + More info: https://questionary.readthedocs.io/en/stable/pages/advanced.html#create-questions-from-dictionaries + """ + + def __init__(self, config: Config, dry_run: bool) -> None: + self.config = config + self.dry_run = dry_run + self.custom_style = questionary.Style( + [ + ("separator", "bold fg:#6C6C6C"), + ("instruction", "fg:#6C6C6C"), + ("highlighted", "bold reverse"), + ("pointer", "bold"), + ] + ) + + clear_screen() + + def load_vault(self, path_filter: str = None) -> None: + """Load the vault. + + Args: + path_filter (str, optional): Regex to filter notes by path. + """ + self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run, path_filter=path_filter) + log.info(f"Indexed {self.vault.num_notes()} notes from {self.vault.vault_path}") + + def main_app(self) -> None: # noqa: C901 + """Questions for the main application.""" + self.load_vault() + + while True: + self.vault.info() + operation = questionary.select( + "What do you want to do?", + choices=[ + questionary.Separator("\n-- VAULT ACTIONS -----------------"), + {"name": "Backup vault", "value": "backup_vault"}, + {"name": "Delete vault backup", "value": "delete_backup"}, + {"name": "View all metadata", "value": "all_metadata"}, + {"name": "List notes in scope", "value": "list_notes"}, + { + "name": "Filter the notes being processed by their path", + "value": "filter_notes", + }, + questionary.Separator("\n-- INLINE TAG ACTIONS ---------"), + questionary.Separator("Tags in the note body"), + { + "name": "Rename an inline tag", + "value": "rename_inline_tag", + }, + { + "name": "Delete an inline tag", + "value": "delete_inline_tag", + }, + questionary.Separator("\n-- METADATA ACTIONS -----------"), + questionary.Separator("Frontmatter or inline metadata"), + {"name": "Rename Key", "value": "rename_key"}, + {"name": "Delete Key", "value": "delete_key"}, + {"name": "Rename Value", "value": "rename_value"}, + {"name": "Delete Value", "value": "delete_value"}, + questionary.Separator("\n-- REVIEW/COMMIT CHANGES ------"), + {"name": "Review changes", "value": "review_changes"}, + {"name": "Commit changes", "value": "commit_changes"}, + questionary.Separator("-------------------------------"), + {"name": "Quit", "value": "abort"}, + ], + use_shortcuts=False, + style=self.custom_style, + ).ask() + + if operation == "filter_notes": + path_filter = questionary.text( + "Enter a regex to filter notes by path", + validate=lambda text: len(text) > 0, + ).ask() + if path_filter is None: + continue + self.load_vault(path_filter=path_filter) + + if operation == "all_metadata": + self.vault.metadata.print_metadata() + + if operation == "backup_vault": + self.vault.backup() + + if operation == "delete_backup": + self.vault.delete_backup() + + if operation == "list_notes": + self.vault.list_editable_notes() + + if operation == "rename_inline_tag": + self.rename_inline_tag() + + if operation == "delete_inline_tag": + self.delete_inline_tag() + + if operation == "rename_key": + self.rename_key() + + if operation == "delete_key": + self.delete_key() + + if operation == "rename_value": + self.rename_value() + + if operation == "delete_value": + self.delete_value() + + if operation == "review_changes": + self.review_changes() + + if operation == "commit_changes": + self.commit_changes() + + if operation == "abort": + break + + print("Done!") + return + + def rename_key(self) -> None: + """Renames a key in the vault.""" + + def validate_key(text: str) -> bool: + """Validate the key name.""" + if self.vault.metadata.contains(text): + return True + return False + + def validate_new_key(text: str) -> bool: + """Validate the tag name.""" + if PATTERNS.validate_key_text.search(text) is not None: + return False + if len(text) == 0: + return False + + return True + + original_key = questionary.text( + "Which key would you like to rename?", + validate=validate_key, + ).ask() + if original_key is None: + return + + new_key = questionary.text( + "New key name", + validate=validate_new_key, + ).ask() + if new_key is None: + return + + self.vault.rename_metadata(original_key, new_key) + + def rename_inline_tag(self) -> None: + """Rename an inline tag.""" + + def validate_new_tag(text: str) -> bool: + """Validate the tag name.""" + if PATTERNS.validate_tag_text.search(text) is not None: + return False + if len(text) == 0: + return False + + return True + + original_tag = questionary.text( + "Which tag would you like to rename?", + validate=lambda text: True + if self.vault.contains_inline_tag(text) + else "Tag not found in vault", + ).ask() + if original_tag is None: + return + + new_tag = questionary.text( + "New tag name", + validate=validate_new_tag, + ).ask() + if new_tag is None: + return + + self.vault.rename_inline_tag(original_tag, new_tag) + alerts.success(f"Renamed [reverse]{original_tag}[/] to [reverse]{new_tag}[/]") + return + + def delete_inline_tag(self) -> None: + """Delete an inline tag.""" + tag = questionary.text( + "Which tag would you like to delete?", + validate=lambda text: True + if self.vault.contains_inline_tag(text) + else "Tag not found in vault", + ).ask() + if tag is None: + return + + self.vault.delete_inline_tag(tag) + alerts.success(f"Deleted inline tag: {tag}") + return + + def delete_key(self) -> None: + """Delete a key from the vault.""" + while True: + key_to_delete = questionary.text("Regex for the key(s) you'd like to delete?").ask() + if key_to_delete is None: + return + + if not self.vault.metadata.contains(key_to_delete, is_regex=True): + alerts.warning(f"No matching keys in the vault: {key_to_delete}") + continue + + num_changed = self.vault.delete_metadata(key_to_delete) + if num_changed == 0: + alerts.warning(f"No notes found matching: [reverse]{key_to_delete}[/]") + return + + alerts.success( + f"Deleted keys matching: [reverse]{key_to_delete}[/] from {num_changed} notes" + ) + break + + return + + def rename_value(self) -> None: + """Rename a value in the vault.""" + key = questionary.text( + "Which key contains the value to rename?", + validate=lambda text: True + if self.vault.metadata.contains(text) + else "Key not found in vault", + ).ask() + if key is None: + return + + value = questionary.text( + "Which value would you like to rename?", + validate=lambda text: True + if self.vault.metadata.contains(key, text) + else f"Value not found in {key}", + ).ask() + if value is None: + return + + new_value = questionary.text( + "New value?", + validate=lambda text: True + if not self.vault.metadata.contains(key, text) + else f"Value already exists in {key}", + ).ask() + + if self.vault.rename_metadata(key, value, new_value): + alerts.success(f"Renamed [reverse]{key}: {value}[/] to [reverse]{key}: {new_value}[/]") + + def delete_value(self) -> None: + """Delete a value from the vault.""" + while True: + key = questionary.text( + "Which key contains the value to delete?", + ).ask() + if key is None: + return + if not self.vault.metadata.contains(key, is_regex=True): + alerts.warning(f"No keys in value match: {key}") + continue + break + + while True: + value = questionary.text( + "Regex for the value to delete", + ).ask() + if value is None: + return + if not self.vault.metadata.contains(key, value, is_regex=True): + alerts.warning(f"No matching key value pairs found in the vault: {key}: {value}") + continue + + num_changed = self.vault.delete_metadata(key, value) + if num_changed == 0: + alerts.warning(f"No notes found matching: [reverse]{key}: {value}[/]") + return + + alerts.success( + f"Deleted {num_changed} entries matching: [reverse]{key}[/]: [reverse]{value}[/]" + ) + + break + + return + + def review_changes(self) -> None: + """Review all changes in the vault.""" + changed_notes = self.vault.get_changed_notes() + + if len(changed_notes) == 0: + alerts.info("No changes to review.") + return + + print(f"\nFound {len(changed_notes)} changed notes in the vault.\n") + answer = questionary.confirm("View diffs of individual files?", default=False).ask() + if not answer: + return + + choices: list[dict[str, Any] | questionary.Separator] = [questionary.Separator()] + for n, note in enumerate(changed_notes, start=1): + _selection = { + "name": f"{n}: {note.note_path.relative_to(self.vault.vault_path)}", + "value": n - 1, + } + choices.append(_selection) + + choices.append(questionary.Separator()) + choices.append({"name": "Return", "value": "skip"}) + + while True: + note_to_review = questionary.select( + "Select a new to view the diff.", + choices=choices, + use_shortcuts=False, + style=self.custom_style, + ).ask() + if note_to_review is None or note_to_review == "skip": + break + changed_notes[note_to_review].print_diff() + + def commit_changes(self) -> None: + """Write all changes to disk.""" + changed_notes = self.vault.get_changed_notes() + + if len(changed_notes) == 0: + print("\n") + alerts.notice("No changes to commit.\n") + return + + backup = questionary.confirm("Create backup before committing changes").ask() + if backup is None: + return + if backup: + self.vault.backup() + + if questionary.confirm(f"Commit {len(changed_notes)} changed files to disk?").ask(): + + self.vault.write() + alerts.success("Changes committed to disk. Exiting.") + typer.Exit() + + return diff --git a/src/obsidian_metadata/models/metadata.py b/src/obsidian_metadata/models/metadata.py new file mode 100644 index 0000000..6de17fa --- /dev/null +++ b/src/obsidian_metadata/models/metadata.py @@ -0,0 +1,505 @@ +"""Work with metadata items.""" + +import re +from io import StringIO + +from rich import print +from rich.columns import Columns +from rich.console import Console +from rich.table import Table +from ruamel.yaml import YAML + +from obsidian_metadata._utils import ( + clean_dictionary, + dict_contains, + dict_values_to_lists_strings, + remove_markdown_sections, +) +from obsidian_metadata.models import Patterns # isort: ignore + +PATTERNS = Patterns() +INLINE_TAG_KEY: str = "Inline Tags" + + +class VaultMetadata: + """Representation of all Metadata in the Vault.""" + + def __init__(self) -> None: + self.dict: dict[str, list[str]] = {} + + def __repr__(self) -> str: + """Representation of all metadata.""" + return str(self.dict) + + def add_metadata(self, metadata: dict[str, list[str]]) -> None: + """Add metadata to the vault. Takes a dictionary as input and merges it with the existing metadata. Does not overwrite existing keys. + + Args: + metadata (dict): Metadata to add. + """ + existing_metadata = self.dict + + new_metadata = clean_dictionary(metadata) + + for k, v in new_metadata.items(): + if k in existing_metadata: + if isinstance(v, list): + existing_metadata[k].extend(v) + else: + existing_metadata[k] = v + + for k, v in existing_metadata.items(): + if isinstance(v, list): + existing_metadata[k] = sorted(set(v)) + elif isinstance(v, dict): + for kk, vv in v.items(): + if isinstance(vv, list): + v[kk] = sorted(set(vv)) + + self.dict = dict(sorted(existing_metadata.items())) + + def print_keys(self) -> None: + """Print all metadata keys.""" + columns = Columns( + sorted(self.dict.keys()), + equal=True, + expand=True, + title="All metadata keys in Obsidian vault", + ) + print(columns) + + def print_tags(self) -> None: + """Print all tags.""" + columns = Columns( + sorted(self.dict["tags"]), + equal=True, + expand=True, + title="All tags in Obsidian vault", + ) + print(columns) + + def print_metadata(self) -> None: + """Print all metadata.""" + table = Table(show_footer=False, show_lines=True) + table.add_column("Keys") + table.add_column("Values") + for key, value in sorted(self.dict.items()): + values: str | dict[str, list[str]] = ( + "\n".join(sorted(value)) if isinstance(value, list) else value + ) + table.add_row(f"[bold]{key}[/]", str(values)) + Console().print(table) + + def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool: + """Check if a key and/or a value exists in the metadata. + + Args: + key (str): Key to check. + value (str, optional): Value to check. + is_regex (bool, optional): Use regex to check. Defaults to False. + + Returns: + bool: True if the key exists. + """ + return dict_contains(self.dict, key, value, is_regex) + + def delete(self, key: str, value_to_delete: str = None) -> bool: + """Delete a key or a key's value from the metadata. Regex is supported to allow deleting more than one key or value. + + Args: + key (str): Key to check. + value_to_delete (str, optional): Value to delete. + + Returns: + bool: True if a value was deleted + """ + new_dict = self.dict.copy() + + if value_to_delete is None: + for _k in list(new_dict): + if re.search(key, _k): + del new_dict[_k] + else: + for _k, _v in new_dict.items(): + if re.search(key, _k): + new_values = [x for x in _v if not re.search(value_to_delete, x)] + new_dict[_k] = sorted(new_values) + + if new_dict != self.dict: + self.dict = dict(new_dict) + return True + + return False + + def rename(self, key: str, value_1: str, value_2: str = None) -> bool: + """Replace a value in the frontmatter. + + Args: + key (str): Key to check. + value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key + value_2 (str, Optional): New value. + bypass_check (bool, optional): Bypass the check if the key exists. Defaults to False. + + Returns: + bool: True if a value was renamed + """ + if value_2 is None: + if key in self.dict and value_1 not in self.dict: + self.dict[value_1] = self.dict.pop(key) + return True + return False + + if key in self.dict and value_1 in self.dict[key]: + self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]}) + return True + + return False + + +class Frontmatter: + """Representation of frontmatter metadata.""" + + def __init__(self, file_content: str): + + self.dict: dict[str, list[str]] = self._grab_note_frontmatter(file_content) + self.dict_original: dict[str, list[str]] = self.dict.copy() + + def __repr__(self) -> str: # pragma: no cover + """Representation of the frontmatter. + + Returns: + str: frontmatter + """ + return f"Frontmatter(frontmatter={self.dict})" + + def _grab_note_frontmatter(self, file_content: str) -> dict: + """Grab metadata from a note. + + Args: + note_path (Path): Path to the note file. + + Returns: + dict: Metadata from the note. + """ + try: + frontmatter_block: str = PATTERNS.frontmatt_block_no_separators.search( + file_content + ).group("frontmatter") + except AttributeError: + return {} + + yaml = YAML(typ="safe") + frontmatter: dict = yaml.load(frontmatter_block) + + for k in frontmatter: + if frontmatter[k] is None: + frontmatter[k] = [] + + return dict_values_to_lists_strings(frontmatter, strip_null_values=True) + + def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool: + """Check if a key or value exists in the metadata. + + Args: + key (str): Key to check. + value (str, optional): Value to check. + is_regex (bool, optional): Use regex to check. Defaults to False. + + Returns: + bool: True if the key exists. + """ + return dict_contains(self.dict, key, value, is_regex) + + def rename(self, key: str, value_1: str, value_2: str = None) -> bool: + """Replace a value in the frontmatter. + + Args: + key (str): Key to check. + value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key + value_2 (str, Optional): New value. + + Returns: + bool: True if a value was renamed + """ + if value_2 is None: + if key in self.dict and value_1 not in self.dict: + self.dict[value_1] = self.dict.pop(key) + return True + return False + + if key in self.dict and value_1 in self.dict[key]: + self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]}) + return True + + return False + + def delete(self, key: str, value_to_delete: str = None) -> bool: + """Delete a value or key in the frontmatter. Regex is supported to allow deleting more than one key or value. + + Args: + key (str): If no value, key to delete. If value, key containing the value. + value_to_delete (str, optional): Value to delete. + + Returns: + bool: True if a value was deleted + """ + new_dict = dict(self.dict) + + if value_to_delete is None: + for _k in list(new_dict): + if re.search(key, _k): + del new_dict[_k] + else: + for _k, _v in new_dict.items(): + if re.search(key, _k): + new_values = [x for x in _v if not re.search(value_to_delete, x)] + new_dict[_k] = sorted(new_values) + + if new_dict != self.dict: + self.dict = dict(new_dict) + return True + + return False + + def has_changes(self) -> bool: + """Check if the frontmatter has changes. + + Returns: + bool: True if the frontmatter has changes. + """ + return self.dict != self.dict_original + + def to_yaml(self, sort_keys: bool = False) -> str: + """Return the frontmatter as a YAML string. + + Returns: + str: Frontmatter as a YAML string. + sort_keys (bool, optional): Sort the keys. Defaults to False. + """ + dict_to_dump = self.dict.copy() + for k in dict_to_dump: + if dict_to_dump[k] == []: + dict_to_dump[k] = None + if isinstance(dict_to_dump[k], list) and len(dict_to_dump[k]) == 1: + new_val = dict_to_dump[k][0] + dict_to_dump[k] = new_val # type: ignore [assignment] + + # Converting stream to string from https://stackoverflow.com/questions/47614862/best-way-to-use-ruamel-yaml-to-dump-yaml-to-string-not-to-stream/63179923#63179923 + + if sort_keys: + dict_to_dump = dict(sorted(dict_to_dump.items())) + + yaml = YAML() + yaml.indent(mapping=2, sequence=4, offset=2) + string_stream = StringIO() + yaml.dump(dict_to_dump, string_stream) + yaml_value = string_stream.getvalue() + string_stream.close() + return yaml_value + + +class InlineMetadata: + """Representation of inline metadata in the form of `key:: value`.""" + + def __init__(self, file_content: str): + + self.dict: dict[str, list[str]] = self._grab_inline_metadata(file_content) + self.dict_original: dict[str, list[str]] = self.dict.copy() + + def __repr__(self) -> str: # pragma: no cover + """Representation of inline metadata. + + Returns: + str: inline metadata + """ + return f"InlineMetadata(inline_metadata={self.dict})" + + def _grab_inline_metadata(self, file_content: str) -> dict[str, list[str]]: + """Grab inline metadata from a note. + + Returns: + dict[str, str]: Inline metadata from the note. + """ + content = remove_markdown_sections( + file_content, + strip_codeblocks=True, + strip_inlinecode=True, + strip_frontmatter=True, + ) + all_results = PATTERNS.find_inline_metadata.findall(content) + stripped_null_values = [tuple(filter(None, x)) for x in all_results] + + inline_metadata: dict[str, list[str]] = {} + for (k, v) in stripped_null_values: + if k in inline_metadata: + inline_metadata[k].append(str(v)) + else: + inline_metadata[k] = [str(v)] + + return clean_dictionary(inline_metadata) + + def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool: + """Check if a key or value exists in the inline metadata. + + Args: + key (str): Key to check. + value (str, Optional): Value to check. + is_regex (bool, optional): If True, key and value are treated as regex. Defaults to False. + + Returns: + bool: True if the key exists. + """ + return dict_contains(self.dict, key, value, is_regex) + + def rename(self, key: str, value_1: str, value_2: str = None) -> bool: + """Replace a value in the inline metadata. + + Args: + key (str): Key to check. + value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key + value_2 (str, Optional): New value. + + Returns: + bool: True if a value was renamed + """ + if value_2 is None: + if key in self.dict and value_1 not in self.dict: + self.dict[value_1] = self.dict.pop(key) + return True + return False + + if key in self.dict and value_1 in self.dict[key]: + self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]}) + return True + + return False + + def delete(self, key: str, value_to_delete: str = None) -> bool: + """Delete a value or key in the inline metadata. Regex is supported to allow deleting more than one key or value. + + Args: + key (str): If no value, key to delete. If value, key containing the value. + value_to_delete (str, optional): Value to delete. + + Returns: + bool: True if a value was deleted + """ + new_dict = dict(self.dict) + + if value_to_delete is None: + for _k in list(new_dict): + if re.search(key, _k): + del new_dict[_k] + else: + for _k, _v in new_dict.items(): + if re.search(key, _k): + new_values = [x for x in _v if not re.search(value_to_delete, x)] + new_dict[_k] = sorted(new_values) + + if new_dict != self.dict: + self.dict = dict(new_dict) + return True + + return False + + def has_changes(self) -> bool: + """Check if the metadata has changes. + + Returns: + bool: True if the metadata has changes. + """ + return self.dict != self.dict_original + + +class InlineTags: + """Representation of inline tags.""" + + def __init__(self, file_content: str): + + self.metadata_key = INLINE_TAG_KEY + self.list: list[str] = self._grab_inline_tags(file_content) + self.list_original: list[str] = self.list.copy() + + def __repr__(self) -> str: # pragma: no cover + """Representation of the inline tags. + + Returns: + str: inline tags + """ + return f"InlineTags(tags={self.list})" + + def _grab_inline_tags(self, file_content: str) -> list[str]: + """Grab inline tags from a note. + + Args: + file_content (str): Total contents of the note file (frontmatter and content). + + Returns: + list[str]: Inline tags from the note. + """ + return sorted( + PATTERNS.find_inline_tags.findall( + remove_markdown_sections( + file_content, + strip_codeblocks=True, + strip_inlinecode=True, + ) + ) + ) + + def contains(self, tag: str, is_regex: bool = False) -> bool: + """Check if a tag exists in the metadata. + + Args: + tag (str): Tag to check. + is_regex (bool, optional): If True, tag is treated as regex. Defaults to False. + + Returns: + bool: True if the tag exists. + """ + if is_regex is True: + return any(re.search(tag, _t) for _t in self.list) + + if tag in self.list: + return True + + return False + + def rename(self, old_tag: str, new_tag: str) -> bool: + """Replace an inline tag with another string. + + Args: + old_tag (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key + new_tag (str, Optional): New value. + + Returns: + bool: True if a value was renamed + """ + if old_tag in self.list: + self.list = sorted([new_tag if i == old_tag else i for i in self.list]) + return True + return False + + def delete(self, tag_to_delete: str) -> bool: + """Delete a specified inline tag. Regex is supported to allow deleting more than one tag. + + Args: + tag_to_delete (str, optional): Value to delete. + + Returns: + bool: True if a value was deleted + """ + new_list = sorted([x for x in self.list if re.search(tag_to_delete, x) is None]) + + if new_list != self.list: + self.list = new_list + return True + return False + + def has_changes(self) -> bool: + """Check if the metadata has changes. + + Returns: + bool: True if the metadata has changes. + """ + return self.list != self.list_original diff --git a/src/obsidian_metadata/models/notes.py b/src/obsidian_metadata/models/notes.py new file mode 100644 index 0000000..7af29ac --- /dev/null +++ b/src/obsidian_metadata/models/notes.py @@ -0,0 +1,367 @@ +"""Representation of notes and in the vault.""" + + +import difflib +import re +from pathlib import Path + +import rich.repr +import typer +from rich import print + +from obsidian_metadata._utils import alerts +from obsidian_metadata._utils.alerts import logger as log +from obsidian_metadata.models import ( + Frontmatter, + InlineMetadata, + InlineTags, + Patterns, +) + +PATTERNS = Patterns() + + +@rich.repr.auto +class Note: + """Representation of a note in the vault. + + Args: + note_path (Path): Path to the note file. + + Attributes: + note_path (Path): Path to the note file. + dry_run (bool): Whether to run in dry-run mode. + file_content (str): Total contents of the note file (frontmatter and content). + frontmatter (dict): Frontmatter of the note. + inline_tags (list): List of inline tags in the note. + inline_metadata (dict): Dictionary of inline metadata in the note. + """ + + def __init__(self, note_path: Path, dry_run: bool = False): + log.trace(f"Creating Note object for {note_path}") + self.note_path: Path = Path(note_path) + self.dry_run: bool = dry_run + + try: + with self.note_path.open(): + self.file_content: str = self.note_path.read_text() + except FileNotFoundError as e: + alerts.error(f"Note {self.note_path} not found. Exiting") + raise typer.Exit(code=1) from e + + self.frontmatter: Frontmatter = Frontmatter(self.file_content) + self.inline_tags: InlineTags = InlineTags(self.file_content) + self.inline_metadata: InlineMetadata = InlineMetadata(self.file_content) + self.original_file_content: str = self.file_content + + def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover + """Define rich representation of Vault.""" + yield "note_path", self.note_path + yield "dry_run", self.dry_run + yield "frontmatter", self.frontmatter + yield "inline_tags", self.inline_tags + yield "inline_metadata", self.inline_metadata + + def append(self, string_to_append: str, allow_multiple: bool = False) -> None: + """Appends a string to the end of a note. + + Args: + string_to_append (str): String to append to the note. + allow_multiple (bool): Whether to allow appending the string if it already exists in the note. + """ + if allow_multiple: + self.file_content += f"\n{string_to_append}" + else: + if len(re.findall(re.escape(string_to_append), self.file_content)) == 0: + self.file_content += f"\n{string_to_append}" + + def commit_changes(self) -> None: + """Commits changes to the note to disk.""" + # TODO: rewrite frontmatter if it has changed + pass + + def contains_inline_tag(self, tag: str, is_regex: bool = False) -> bool: + """Check if a note contains the specified inline tag. + + Args: + tag (str): Tag to check for. + is_regex (bool, optional): Whether to use regex to match the tag. + + Returns: + bool: Whether the note has inline tags. + """ + return self.inline_tags.contains(tag, is_regex=is_regex) + + def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool: + """Check if a note has a key or a key-value pair in its metadata. + + Args: + key (str): Key to check for. + value (str, optional): Value to check for. + is_regex (bool, optional): Whether to use regex to match the key/value. + + Returns: + bool: Whether the note contains the key or key-value pair. + """ + if value is None: + if self.frontmatter.contains(key, is_regex=is_regex) or self.inline_metadata.contains( + key, is_regex=is_regex + ): + return True + return False + + if self.frontmatter.contains( + key, value, is_regex=is_regex + ) or self.inline_metadata.contains(key, value, is_regex=is_regex): + return True + + return False + + def _delete_inline_metadata(self, key: str, value: str = None) -> None: + """Deletes an inline metadata key/value pair from the text of the note. This method does not remove the key/value from the metadata attribute of the note. + + Args: + key (str): Key to delete. + value (str, optional): Value to delete. + """ + all_results = PATTERNS.find_inline_metadata.findall(self.file_content) + stripped_null_values = [tuple(filter(None, x)) for x in all_results] + + for (_k, _v) in stripped_null_values: + if re.search(key, _k): + if value is None: + _k = re.escape(_k) + _v = re.escape(_v) + self.sub(rf"\[?{_k}:: ?{_v}]?", "", is_regex=True) + return + + if re.search(value, _v): + _k = re.escape(_k) + _v = re.escape(_v) + self.sub(rf"({_k}::) ?{_v}", r"\1", is_regex=True) + + def delete_inline_tag(self, tag: str) -> bool: + """Deletes an inline tag from the `inline_tags` attribute AND removes the tag from the text of the note if it exists. + + Args: + tag (str): Tag to delete. + + Returns: + bool: Whether the tag was deleted. + """ + new_list = self.inline_tags.list.copy() + + for _t in new_list: + if re.search(tag, _t): + _t = re.escape(_t) + self.sub(rf"#{_t}([ \|,;:\*\(\)\[\]\\\.\n#&])", r"\1", is_regex=True) + self.inline_tags.delete(tag) + + if new_list != self.inline_tags.list: + return True + + return False + + def delete_metadata(self, key: str, value: str = None) -> bool: + """Deletes a key or key-value pair from the note's metadata. Regex is supported. + + If no value is provided, will delete an entire key. + + Args: + key (str): Key to delete. + value (str, optional): Value to delete. + + Returns: + bool: Whether the key or key-value pair was deleted. + """ + changed_value: bool = False + + if value is None: + if self.frontmatter.delete(key): + self.replace_frontmatter() + changed_value = True + if self.inline_metadata.delete(key): + self._delete_inline_metadata(key, value) + changed_value = True + else: + if self.frontmatter.delete(key, value): + self.replace_frontmatter() + changed_value = True + if self.inline_metadata.delete(key, value): + self._delete_inline_metadata(key, value) + changed_value = True + + if changed_value: + return True + return False + + def has_changes(self) -> bool: + """Checks if the note has been updated. + + Returns: + bool: Whether the note has been updated. + """ + if self.frontmatter.has_changes(): + return True + + if self.inline_tags.has_changes(): + return True + + if self.inline_metadata.has_changes(): + return True + + if self.file_content != self.original_file_content: + return True + + return False + + def print_note(self) -> None: + """Prints the note to the console.""" + print(self.file_content) + + def print_diff(self) -> None: + """Prints a diff of the note's original state and it's new state.""" + a = self.original_file_content.splitlines() + b = self.file_content.splitlines() + + diff = difflib.Differ() + result = list(diff.compare(a, b)) + for line in result: + if line.startswith("+"): + print(f"[green]{line}[/]") + elif line.startswith("-"): + print(f"[red]{line}[/]") + + def sub(self, pattern: str, replacement: str, is_regex: bool = False) -> None: + """Substitutes text within the note. + + Args: + pattern (str): The pattern to replace (plain text or regular expression). + replacement (str): What to replace the pattern with. + is_regex (bool): Whether the pattern is a regex pattern or plain text. + """ + if not is_regex: + pattern = re.escape(pattern) + + self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE) + + def _rename_inline_metadata(self, key: str, value_1: str, value_2: str = None) -> None: + """Replaces the inline metadata in the note with the current inline metadata object. + + Args: + key (str): Key to rename. + value_1 (str): Value to replace OR new key name (if value_2 is None). + value_2 (str, optional): New value. + + """ + all_results = PATTERNS.find_inline_metadata.findall(self.file_content) + stripped_null_values = [tuple(filter(None, x)) for x in all_results] + + for (_k, _v) in stripped_null_values: + if re.search(key, _k): + if value_2 is None: + if re.search(rf"{key}[^\w\d_-]+", _k): + key_text = re.split(r"[^\w\d_-]+$", _k)[0] + key_markdown = re.split(r"^[\w\d_-]+", _k)[1] + self.sub( + rf"{key_text}{key_markdown}::", + rf"{value_1}{key_markdown}::", + ) + else: + self.sub(f"{_k}::", f"{value_1}::") + else: + if re.search(key, _k) and re.search(value_1, _v): + _k = re.escape(_k) + _v = re.escape(_v) + self.sub(f"{_k}:: ?{_v}", f"{_k}:: {value_2}", is_regex=True) + + def rename_inline_tag(self, tag_1: str, tag_2: str) -> bool: + """Renames an inline tag from the note ONLY if it's not in the metadata as well. + + Args: + tag_1 (str): Tag to rename. + tag_2 (str): New tag name. + + Returns: + bool: Whether the tag was renamed. + """ + if tag_1 in self.inline_tags.list: + self.sub( + rf"#{tag_1}([ \|,;:\*\(\)\[\]\\\.\n#&])", + rf"#{tag_2}\1", + is_regex=True, + ) + self.inline_tags.rename(tag_1, tag_2) + return True + return False + + def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> bool: + """Renames a key or key-value pair in the note's metadata. + + If no value is provided, will rename an entire key. + + Args: + key (str): Key to rename. + value_1 (str): Value to rename or new name of key if no value_2 is provided. + value_2 (str, optional): New value. + + Returns: + bool: Whether the note was updated. + """ + changed_value: bool = False + if value_2 is None: + if self.frontmatter.rename(key, value_1): + self.replace_frontmatter() + changed_value = True + if self.inline_metadata.rename(key, value_1): + self._rename_inline_metadata(key, value_1) + changed_value = True + else: + if self.frontmatter.rename(key, value_1, value_2): + self.replace_frontmatter() + changed_value = True + if self.inline_metadata.rename(key, value_1, value_2): + self._rename_inline_metadata(key, value_1, value_2) + changed_value = True + + if changed_value: + return True + + return False + + def replace_frontmatter(self, sort_keys: bool = False) -> None: + """Replaces the frontmatter in the note with the current frontmatter object.""" + try: + current_frontmatter = PATTERNS.frontmatt_block_with_separators.search( + self.file_content + ).group("frontmatter") + except AttributeError: + current_frontmatter = None + + if current_frontmatter is None and self.frontmatter.dict == {}: + return + + new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys) + new_frontmatter = f"---\n{new_frontmatter}---\n" + + if current_frontmatter is None: + self.file_content = new_frontmatter + self.file_content + return + + self.sub(current_frontmatter, new_frontmatter) + + def write(self, path: Path | None = None) -> None: + """Writes the note's content to disk. + + Args: + path (Path): Path to write the note to. Defaults to the note's path. + """ + p = self.note_path if path is None else path + + try: + with open(p, "w") as f: + log.trace(f"Writing note {p} to disk") + f.write(self.file_content) + except FileNotFoundError as e: + alerts.error(f"Note {p} not found. Exiting") + raise typer.Exit(code=1) from e diff --git a/src/obsidian_metadata/models/patterns.py b/src/obsidian_metadata/models/patterns.py new file mode 100644 index 0000000..9e72eb8 --- /dev/null +++ b/src/obsidian_metadata/models/patterns.py @@ -0,0 +1,41 @@ +"""Regexes for parsing frontmatter and note content.""" + +import re +from dataclasses import dataclass +from typing import Pattern + + +@dataclass +class Patterns: + """Regex patterns for parsing frontmatter and note content.""" + + find_inline_tags: Pattern[str] = re.compile( + r""" + (?:^|[ \|_,;:\*\(\)\[\]\\\.]) # Before tag is start of line or separator + \#([^ \|,;:\*\(\)\[\]\\\.\n#&]+) # Match tag until separator or end of line + """, + re.MULTILINE | re.X, + ) + + frontmatt_block_with_separators: Pattern[str] = re.compile( + r"^\s*(?P---.*?---)", flags=re.DOTALL + ) + frontmatt_block_no_separators: Pattern[str] = re.compile( + r"^\s*---(?P.*?)---", flags=re.DOTALL + ) + # This pattern will return a tuple of 4 values, two will be empty and will need to be stripped before processing further + find_inline_metadata: Pattern[str] = re.compile( + r""" # First look for in-text key values + (?:^\[| \[) # Find key with starting bracket + ([-_\w\d\/\*\u263a-\U0001f645]+?)::[ ]? # Find key + (.*?)\] # Find value until closing bracket + | # Else look for key values at start of line + (?:^|[^ \w\d]+| \[) # Any non-word or non-digit character + ([-_\w\d\/\*\u263a-\U0001f645]+?)::(?!\n)(?:[ ](?!\n))? # Capture the key if not a new line + (.*?)$ # Capture the value + """, + re.X | re.MULTILINE, + ) + + validate_tag_text: Pattern[str] = re.compile(r"[ \|,;:\*\(\)\[\]\\\.\n#&]") + validate_key_text: Pattern[str] = re.compile(r"[^-_\w\d\/\*\u263a-\U0001f645]") diff --git a/src/obsidian_metadata/models/vault.py b/src/obsidian_metadata/models/vault.py new file mode 100644 index 0000000..5f3236c --- /dev/null +++ b/src/obsidian_metadata/models/vault.py @@ -0,0 +1,302 @@ +"""Obsidian vault representation.""" + +import re +import shutil +from pathlib import Path + +import rich.repr +from rich.console import Console +from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.prompt import Confirm +from rich.table import Table + +from obsidian_metadata._config import Config +from obsidian_metadata._utils import alerts +from obsidian_metadata._utils.alerts import logger as log +from obsidian_metadata.models import Note, VaultMetadata + + +@rich.repr.auto +class Vault: + """Representation of the Obsidian vault. + + Attributes: + vault (Path): Path to the vault. + dry_run (bool): Whether to perform a dry run. + backup_path (Path): Path to the backup of the vault. + new_vault (Path): Path to a new vault. + notes (list[Note]): List of all notes in the vault. + """ + + def __init__(self, config: Config, dry_run: bool = False, path_filter: str = None): + self.vault_path: Path = config.vault_path + self.dry_run: bool = dry_run + self.backup_path: Path = self.vault_path.parent / f"{self.vault_path.name}.bak" + self.new_vault_path: Path = self.vault_path.parent / f"{self.vault_path.name}.new" + self.exclude_paths: list[Path] = [] + self.metadata = VaultMetadata() + for p in config.exclude_paths: + self.exclude_paths.append(Path(self.vault_path / p)) + + self.path_filter = path_filter + self.note_paths = self._find_markdown_notes(path_filter) + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + transient=True, + ) as progress: + progress.add_task(description="Processing notes...", total=None) + self.notes: list[Note] = [ + Note(note_path=p, dry_run=self.dry_run) for p in self.note_paths + ] + for _note in self.notes: + self.metadata.add_metadata(_note.frontmatter.dict) + self.metadata.add_metadata(_note.inline_metadata.dict) + self.metadata.add_metadata({_note.inline_tags.metadata_key: _note.inline_tags.list}) + + def __rich_repr__(self) -> rich.repr.Result: + """Define rich representation of Vault.""" + yield "vault_path", self.vault_path + yield "dry_run", self.dry_run + yield "backup_path", self.backup_path + yield "new_vault", self.new_vault_path + yield "num_notes", self.num_notes() + yield "exclude_paths", self.exclude_paths + + def _find_markdown_notes(self, path_filter: str = None) -> list[Path]: + """Build list of all markdown files in the vault. + + Args: + path_filter (str, optional): Regex to filter notes by path. + + Returns: + list[Path]: List of paths to all matching files in the vault. + + """ + notes_list = [ + p.resolve() + for p in self.vault_path.glob("**/*") + if p.suffix in [".md", ".MD", ".markdown", ".MARKDOWN"] + and not any(item in p.parents for item in self.exclude_paths) + ] + + if path_filter is not None: + notes_list = [ + p for p in notes_list if re.search(path_filter, str(p.relative_to(self.vault_path))) + ] + + return notes_list + + def backup(self) -> None: + """Backup the vault.""" + log.debug("Backing up vault") + if self.dry_run: + alerts.dryrun(f"Backup up vault to: {self.backup_path}") + return + + try: + shutil.copytree(self.vault_path, self.backup_path) + + except FileExistsError: # pragma: no cover + log.debug("Backup already exists") + if not Confirm.ask("Vault backup already exists. Overwrite?"): + alerts.info("Exiting backup not overwritten.") + return + + log.debug("Overwriting backup") + shutil.rmtree(self.backup_path) + shutil.copytree(self.vault_path, self.backup_path) + + alerts.success(f"Vault backed up to: {self.backup_path}") + + def contains_inline_tag(self, tag: str, is_regex: bool = False) -> bool: + """Check if vault contains the given inline tag. + + Args: + tag (str): Tag to check for. + is_regex (bool, optional): Whether to use regex to match tag. + + Returns: + bool: True if tag is found in vault. + """ + return any(_note.contains_inline_tag(tag) for _note in self.notes) + + def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool: + """Check if vault contains the given metadata. + + Args: + key (str): Key to check for. If value is None, will check vault for key. + value (str, optional): Value to check for. + is_regex (bool, optional): Whether to use regex to match key/value. + + Returns: + bool: True if tag is found in vault. + """ + if value is None: + return self.metadata.contains(key, is_regex=is_regex) + + return self.metadata.contains(key, value, is_regex=is_regex) + + def delete_backup(self) -> None: + """Delete the vault backup.""" + log.debug("Deleting vault backup") + if self.backup_path.exists() and self.dry_run is False: + shutil.rmtree(self.backup_path) + alerts.success("Backup deleted") + elif self.backup_path.exists() and self.dry_run is True: + alerts.dryrun("Delete backup") + else: + alerts.info("No backup found") + + def delete_inline_tag(self, tag: str) -> bool: + """Delete an inline tag in the vault. + + Args: + tag (str): Tag to delete. + + Returns: + bool: True if tag was deleted. + """ + changes = False + + for _note in self.notes: + if _note.delete_inline_tag(tag): + changes = True + + if changes: + self.metadata.delete(self.notes[0].inline_tags.metadata_key, tag) + return True + return False + + def delete_metadata(self, key: str, value: str = None) -> int: + """Delete metadata in the vault. + + Args: + key (str): Key to delete. Regex is supported + value (str, optional): Value to delete. Regex is supported + + Returns: + int: Number of notes that had metadata deleted. + """ + num_changed = 0 + + for _note in self.notes: + if _note.delete_metadata(key, value): + num_changed += 1 + + if num_changed > 0: + self.metadata.delete(key, value) + return num_changed + return num_changed + + def get_changed_notes(self) -> list[Note]: + """Returns a list of notes that have changes. + + Returns: + list[Note]: List of notes that have changes. + """ + changed_notes = [] + for _note in self.notes: + if _note.has_changes(): + changed_notes.append(_note) + + changed_notes = sorted(changed_notes, key=lambda x: x.note_path) + return changed_notes + + def info(self) -> None: + """Print information about the vault.""" + log.debug("Printing vault info") + table = Table(title="Vault Info", show_header=False) + table.add_row("Vault", str(self.vault_path)) + table.add_row("Notes being edited", str(self.num_notes())) + table.add_row("Notes excluded from editing", str(self.num_excluded_notes())) + if self.backup_path.exists(): + table.add_row("Backup path", str(self.backup_path)) + else: + table.add_row("Backup", "None") + table.add_row("Active path filter", str(self.path_filter)) + table.add_row("Notes with updates", str(len(self.get_changed_notes()))) + + Console().print(table) + + def list_editable_notes(self) -> None: + """Print a list of notes within the scope that are being edited.""" + for _note in self.notes: + print(_note.note_path.relative_to(self.vault_path)) + + def num_excluded_notes(self) -> int: + """Count number of excluded notes.""" + excluded_notes = [ + p.resolve() + for p in self.vault_path.glob("**/*") + if p.suffix in [".md", ".MD", ".markdown", ".MARKDOWN"] and p not in self.note_paths + ] + return len(excluded_notes) + + def num_notes(self) -> int: + """Number of notes in the vault. + + Returns: + int: Number of notes in the vault. + """ + return len(self.notes) + + def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> bool: + """Renames a key or key-value pair in the note's metadata. + + If no value is provided, will rename an entire key. + + Args: + key (str): Key to rename. + value_1 (str): Value to rename or new name of key if no value_2 is provided. + value_2 (str, optional): New value. + + Returns: + bool: True if metadata was renamed. + """ + changes = False + for _note in self.notes: + if _note.rename_metadata(key, value_1, value_2): + changes = True + + if changes: + self.metadata.rename(key, value_1, value_2) + return True + return False + + def rename_inline_tag(self, old_tag: str, new_tag: str) -> bool: + """Rename an inline tag in the vault. + + Args: + old_tag (str): Old tag name. + new_tag (str): New tag name. + + Returns: + bool: True if tag was renamed. + """ + changes = False + for _note in self.notes: + if _note.rename_inline_tag(old_tag, new_tag): + changes = True + + if changes: + self.metadata.rename(self.notes[0].inline_tags.metadata_key, old_tag, new_tag) + return True + return False + + def write(self, new_vault: bool = False) -> None: + """Write changes to the vault.""" + log.debug("Writing changes to vault...") + if new_vault: + log.debug("Writing changes to backup") + for _note in self.notes: + _new_note_path: Path = Path( + self.new_vault_path / Path(_note.note_path).relative_to(self.vault_path) + ) + log.debug(f"writing to {_new_note_path}") + _note.write(path=_new_note_path) + else: + for _note in self.notes: + log.debug(f"writing to {_note.note_path}") + _note.write() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..bad9b53 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""obsidian-metadata test suite.""" diff --git a/tests/alerts_test.py b/tests/alerts_test.py new file mode 100644 index 0000000..ef88072 --- /dev/null +++ b/tests/alerts_test.py @@ -0,0 +1,155 @@ +# type: ignore +"""Test alerts and logging.""" +import re + +import pytest + +from obsidian_metadata._utils import alerts +from obsidian_metadata._utils.alerts import logger as log +from tests.helpers import Regex + + +def test_dryrun(capsys): + """Test dry run.""" + alerts.dryrun("This prints in dry run") + captured = capsys.readouterr() + assert captured.out == "DRYRUN | This prints in dry run\n" + + +def test_success(capsys): + """Test success.""" + alerts.success("This prints in success") + captured = capsys.readouterr() + assert captured.out == "SUCCESS | This prints in success\n" + + +def test_error(capsys): + """Test success.""" + alerts.error("This prints in error") + captured = capsys.readouterr() + assert captured.out == "ERROR | This prints in error\n" + + +def test_warning(capsys): + """Test warning.""" + alerts.warning("This prints in warning") + captured = capsys.readouterr() + assert captured.out == "WARNING | This prints in warning\n" + + +def test_notice(capsys): + """Test notice.""" + alerts.notice("This prints in notice") + captured = capsys.readouterr() + assert captured.out == "NOTICE | This prints in notice\n" + + +def test_info(capsys): + """Test info.""" + alerts.info("This prints in info") + captured = capsys.readouterr() + assert captured.out == "INFO | This prints in info\n" + + +def test_dim(capsys): + """Test info.""" + alerts.dim("This prints in dim") + captured = capsys.readouterr() + assert captured.out == "This prints in dim\n" + + +@pytest.mark.parametrize( + ("verbosity", "log_to_file"), + [(0, False), (1, False), (2, True), (3, True)], +) +def test_logging(capsys, tmp_path, verbosity, log_to_file) -> None: + """Test logging.""" + tmp_log = tmp_path / "tmp.log" + logging = alerts.LoggerManager( + log_file=tmp_log, + verbosity=verbosity, + log_to_file=log_to_file, + ) + + assert logging.verbosity == verbosity + + if verbosity >= 3: + assert logging.is_trace() is True + captured = capsys.readouterr() + assert captured.out == "" + + assert logging.is_trace("trace text") is True + captured = capsys.readouterr() + assert captured.out == "trace text\n" + + log.trace("This is Trace logging") + captured = capsys.readouterr() + assert captured.err == Regex(r"^TRACE \| This is Trace logging \([\w\._:]+:\d+\)$") + else: + assert logging.is_trace("trace text") is False + captured = capsys.readouterr() + assert captured.out != "trace text\n" + + log.trace("This is Trace logging") + captured = capsys.readouterr() + assert captured.err != Regex(r"^TRACE \| This is Trace logging \([\w\._:]+:\d+\)$") + + if verbosity >= 2: + assert logging.is_debug() is True + captured = capsys.readouterr() + assert captured.out == "" + + assert logging.is_debug("debug text") is True + captured = capsys.readouterr() + assert captured.out == "debug text\n" + + log.debug("This is Debug logging") + captured = capsys.readouterr() + assert captured.err == Regex(r"^DEBUG \| This is Debug logging \([\w\._:]+:\d+\)$") + else: + assert logging.is_debug("debug text") is False + captured = capsys.readouterr() + assert captured.out != "debug text\n" + + log.debug("This is Debug logging") + captured = capsys.readouterr() + assert captured.err != Regex(r"^DEBUG \| This is Debug logging \([\w\._:]+:\d+\)$") + + if verbosity >= 1: + assert logging.is_info() is True + captured = capsys.readouterr() + assert captured.out == "" + + assert logging.is_info("info text") is True + captured = capsys.readouterr() + assert captured.out == "info text\n" + + log.info("This is Info logging") + captured = capsys.readouterr() + assert captured.err == "INFO | This is Info logging\n" + else: + assert logging.is_info("info text") is False + captured = capsys.readouterr() + assert captured.out != "info text\n" + + log.info("This is Info logging") + captured = capsys.readouterr() + assert captured.out == "" + + assert logging.is_default() is True + captured = capsys.readouterr() + assert captured.out == "" + + assert logging.is_default("default text") is True + captured = capsys.readouterr() + assert captured.out == "default text\n" + + if log_to_file: + assert tmp_log.exists() is True + log_file_content = tmp_log.read_text() + assert log_file_content == Regex( + r"^\d{4}-\d{2}-\d{2} \d+:\d+:\d+\.\d+ \| DEBUG \| [\w\.:]+:\d+ \- Logging to file:", + re.MULTILINE, + ) + else: + assert tmp_log.exists() is False diff --git a/tests/cli_test.py b/tests/cli_test.py new file mode 100644 index 0000000..e00c8f1 --- /dev/null +++ b/tests/cli_test.py @@ -0,0 +1,16 @@ +# type: ignore +"""Test obsidian-metadata CLI.""" + +from typer.testing import CliRunner + +from obsidian_metadata.cli import app +from tests.helpers import Regex + +runner = CliRunner() + + +def test_version() -> None: + """Test printing version and then exiting.""" + result = runner.invoke(app, ["--version"]) + assert result.exit_code == 0 + assert result.output == Regex(r"obsidian_metadata: v\d+\.\d+\.\d+$") diff --git a/tests/config_test.py b/tests/config_test.py new file mode 100644 index 0000000..3873bc6 --- /dev/null +++ b/tests/config_test.py @@ -0,0 +1,28 @@ +# type: ignore +"""Tests for the configuration module.""" + +import re +from pathlib import Path + +from obsidian_metadata._config import Config + + +def test_first_run(tmp_path): + """Test creating a config on first run.""" + config_file = Path(tmp_path / "config.toml") + vault_path = Path(tmp_path / "vault/") + vault_path.mkdir() + + config = Config(config_path=config_file, vault_path=vault_path) + + assert config_file.exists() is True + config.write_config_value("vault", str(vault_path)) + content = config_file.read_text() + assert config.vault_path == vault_path + assert re.search(str(vault_path), content) is not None + + +def test_parse_config(): + """Test parsing a config file.""" + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=None) + assert config.vault_path == Path(Path.cwd() / "tests/fixtures/test_vault") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4656d8d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,74 @@ +# type: ignore +"""Fixtures for tests.""" + +import shutil +from pathlib import Path + +import pytest + + +def remove_all(root: Path): + """Remove all files and directories in a directory.""" + for path in root.iterdir(): + if path.is_file(): + print(f"Deleting the file: {path}") + path.unlink() + else: + remove_all(path) + print(f"Deleting the empty dir: {root}") + root.rmdir() + + +@pytest.fixture() +def sample_note(tmp_path) -> Path: + """Fixture which creates a temporary note file.""" + source_file: Path = Path("tests/fixtures/test_vault/test1.md") + if not source_file.exists(): + raise FileNotFoundError(f"Original file not found: {source_file}") + + dest_file: Path = Path(tmp_path / source_file.name) + shutil.copy(source_file, dest_file) + yield dest_file + + # after test - remove fixtures + dest_file.unlink() + + +@pytest.fixture() +def sample_vault(tmp_path) -> Path: + """Fixture which creates a sample vault.""" + source_dir = Path(__file__).parent / "fixtures" / "sample_vault" + dest_dir = Path(tmp_path / "vault") + backup_dir = Path(f"{dest_dir}.bak") + + if not source_dir.exists(): + raise FileNotFoundError(f"Sample vault not found: {source_dir}") + + shutil.copytree(source_dir, dest_dir) + yield dest_dir + + # after test - remove fixtures + shutil.rmtree(dest_dir) + + if backup_dir.exists(): + shutil.rmtree(backup_dir) + + +@pytest.fixture() +def test_vault(tmp_path) -> Path: + """Fixture which creates a sample vault.""" + source_dir = Path(__file__).parent / "fixtures" / "test_vault" + dest_dir = Path(tmp_path / "vault") + backup_dir = Path(f"{dest_dir}.bak") + + if not source_dir.exists(): + raise FileNotFoundError(f"Sample vault not found: {source_dir}") + + shutil.copytree(source_dir, dest_dir) + yield dest_dir + + # after test - remove fixtures + shutil.rmtree(dest_dir) + + if backup_dir.exists(): + shutil.rmtree(backup_dir) diff --git a/tests/fixtures/sample_note.md b/tests/fixtures/sample_note.md new file mode 100644 index 0000000..9ed4690 --- /dev/null +++ b/tests/fixtures/sample_note.md @@ -0,0 +1,39 @@ +--- +date_created: 2022-12-22 +tags: + - food/fruit/apple + - dinner + - breakfast + - not_food +author: John Doe +nested_list: + nested_list_one: + - nested_list_one_a + - nested_list_one_b +type: +- article +- note +--- + +area:: mixed +date_modified:: 2022-12-22 +status:: new +type:: book +inline_key:: inline_key_value +type:: [[article]] +tags:: from_inline_metadata +**bold_key**:: **bold** key value + + + + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, [in_text_key:: in-text value] eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? #inline_tag + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, #inline_tag2 cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. + +#food/fruit/pear +#food/fruit/orange +#dinner #breakfast +#brunch diff --git a/tests/fixtures/sample_vault/+inbox/Untitled.md b/tests/fixtures/sample_vault/+inbox/Untitled.md new file mode 100644 index 0000000..cb98cbb --- /dev/null +++ b/tests/fixtures/sample_vault/+inbox/Untitled.md @@ -0,0 +1,9 @@ +--- +type: note +tags: + - foo + - bar + - baz + - food/fruit/apple +--- +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/tests/fixtures/sample_vault/.obsidian/app.json b/tests/fixtures/sample_vault/.obsidian/app.json new file mode 100644 index 0000000..6abe4c1 --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/app.json @@ -0,0 +1,3 @@ +{ + "alwaysUpdateLinks": true +} \ No newline at end of file diff --git a/tests/fixtures/sample_vault/.obsidian/appearance.json b/tests/fixtures/sample_vault/.obsidian/appearance.json new file mode 100644 index 0000000..c8c365d --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/appearance.json @@ -0,0 +1,3 @@ +{ + "accentColor": "" +} \ No newline at end of file diff --git a/tests/fixtures/sample_vault/.obsidian/community-plugins.json b/tests/fixtures/sample_vault/.obsidian/community-plugins.json new file mode 100644 index 0000000..3d14383 --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/community-plugins.json @@ -0,0 +1,3 @@ +[ + "templater-obsidian" +] \ No newline at end of file diff --git a/tests/fixtures/sample_vault/.obsidian/core-plugins-migration.json b/tests/fixtures/sample_vault/.obsidian/core-plugins-migration.json new file mode 100644 index 0000000..3106ebd --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/core-plugins-migration.json @@ -0,0 +1,29 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "starred": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": false +} \ No newline at end of file diff --git a/tests/fixtures/sample_vault/.obsidian/core-plugins.json b/tests/fixtures/sample_vault/.obsidian/core-plugins.json new file mode 100644 index 0000000..086a8e4 --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/core-plugins.json @@ -0,0 +1,20 @@ +[ + "file-explorer", + "global-search", + "switcher", + "graph", + "backlink", + "canvas", + "outgoing-link", + "tag-pane", + "page-preview", + "daily-notes", + "templates", + "note-composer", + "command-palette", + "editor-status", + "starred", + "outline", + "word-count", + "file-recovery" +] \ No newline at end of file diff --git a/tests/fixtures/sample_vault/.obsidian/hotkeys.json b/tests/fixtures/sample_vault/.obsidian/hotkeys.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/hotkeys.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/main.js b/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/main.js new file mode 100644 index 0000000..3226253 --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/main.js @@ -0,0 +1,5617 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ + +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __markAsModule = (target) => __defProp(target, "__esModule", { value: true }); +var __export = (target, all) => { + __markAsModule(target); + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __reExport = (target, module2, desc) => { + if (module2 && typeof module2 === "object" || typeof module2 === "function") { + for (let key of __getOwnPropNames(module2)) + if (!__hasOwnProp.call(target, key) && key !== "default") + __defProp(target, key, { get: () => module2[key], enumerable: !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable }); + } + return target; +}; +var __toModule = (module2) => { + return __reExport(__markAsModule(__defProp(module2 != null ? __create(__getProtoOf(module2)) : {}, "default", module2 && module2.__esModule && "default" in module2 ? { get: () => module2.default, enumerable: true } : { value: module2, enumerable: true })), module2); +}; +var __toBinary = /* @__PURE__ */ (() => { + var table = new Uint8Array(128); + for (var i = 0; i < 64; i++) + table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i; + return (base64) => { + var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0); + for (var i2 = 0, j = 0; i2 < n; ) { + var c0 = table[base64.charCodeAt(i2++)], c1 = table[base64.charCodeAt(i2++)]; + var c2 = table[base64.charCodeAt(i2++)], c3 = table[base64.charCodeAt(i2++)]; + bytes[j++] = c0 << 2 | c1 >> 4; + bytes[j++] = c1 << 4 | c2 >> 2; + bytes[j++] = c2 << 6 | c3; + } + return bytes; + }; +})(); + +// src/main.ts +__export(exports, { + default: () => TemplaterPlugin +}); +var import_obsidian18 = __toModule(require("obsidian")); + +// src/settings/Settings.ts +var import_obsidian6 = __toModule(require("obsidian")); + +// src/utils/Log.ts +var import_obsidian = __toModule(require("obsidian")); +function log_error(e) { + const notice = new import_obsidian.Notice("", 8e3); + if (e instanceof TemplaterError && e.console_msg) { + notice.noticeEl.innerHTML = `Templater Error:
${e.message}
Check console for more information`; + console.error(`Templater Error:`, e.message, "\n", e.console_msg); + } else { + notice.noticeEl.innerHTML = `Templater Error:
${e.message}`; + } +} + +// src/utils/Error.ts +var TemplaterError = class extends Error { + constructor(msg, console_msg) { + super(msg); + this.console_msg = console_msg; + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +}; +async function errorWrapper(fn2, msg) { + try { + return await fn2(); + } catch (e) { + if (!(e instanceof TemplaterError)) { + log_error(new TemplaterError(msg, e.message)); + } else { + log_error(e); + } + return null; + } +} +function errorWrapperSync(fn2, msg) { + try { + return fn2(); + } catch (e) { + log_error(new TemplaterError(msg, e.message)); + return null; + } +} + +// src/settings/suggesters/FolderSuggester.ts +var import_obsidian3 = __toModule(require("obsidian")); + +// src/settings/suggesters/suggest.ts +var import_obsidian2 = __toModule(require("obsidian")); + +// node_modules/@popperjs/core/lib/enums.js +var top = "top"; +var bottom = "bottom"; +var right = "right"; +var left = "left"; +var auto = "auto"; +var basePlacements = [top, bottom, right, left]; +var start = "start"; +var end = "end"; +var clippingParents = "clippingParents"; +var viewport = "viewport"; +var popper = "popper"; +var reference = "reference"; +var variationPlacements = /* @__PURE__ */ basePlacements.reduce(function(acc, placement) { + return acc.concat([placement + "-" + start, placement + "-" + end]); +}, []); +var placements = /* @__PURE__ */ [].concat(basePlacements, [auto]).reduce(function(acc, placement) { + return acc.concat([placement, placement + "-" + start, placement + "-" + end]); +}, []); +var beforeRead = "beforeRead"; +var read = "read"; +var afterRead = "afterRead"; +var beforeMain = "beforeMain"; +var main = "main"; +var afterMain = "afterMain"; +var beforeWrite = "beforeWrite"; +var write = "write"; +var afterWrite = "afterWrite"; +var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite]; + +// node_modules/@popperjs/core/lib/dom-utils/getNodeName.js +function getNodeName(element) { + return element ? (element.nodeName || "").toLowerCase() : null; +} + +// node_modules/@popperjs/core/lib/dom-utils/getWindow.js +function getWindow(node) { + if (node == null) { + return window; + } + if (node.toString() !== "[object Window]") { + var ownerDocument = node.ownerDocument; + return ownerDocument ? ownerDocument.defaultView || window : window; + } + return node; +} + +// node_modules/@popperjs/core/lib/dom-utils/instanceOf.js +function isElement(node) { + var OwnElement = getWindow(node).Element; + return node instanceof OwnElement || node instanceof Element; +} +function isHTMLElement(node) { + var OwnElement = getWindow(node).HTMLElement; + return node instanceof OwnElement || node instanceof HTMLElement; +} +function isShadowRoot(node) { + if (typeof ShadowRoot === "undefined") { + return false; + } + var OwnElement = getWindow(node).ShadowRoot; + return node instanceof OwnElement || node instanceof ShadowRoot; +} + +// node_modules/@popperjs/core/lib/modifiers/applyStyles.js +function applyStyles(_ref) { + var state = _ref.state; + Object.keys(state.elements).forEach(function(name) { + var style = state.styles[name] || {}; + var attributes = state.attributes[name] || {}; + var element = state.elements[name]; + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } + Object.assign(element.style, style); + Object.keys(attributes).forEach(function(name2) { + var value = attributes[name2]; + if (value === false) { + element.removeAttribute(name2); + } else { + element.setAttribute(name2, value === true ? "" : value); + } + }); + }); +} +function effect(_ref2) { + var state = _ref2.state; + var initialStyles = { + popper: { + position: state.options.strategy, + left: "0", + top: "0", + margin: "0" + }, + arrow: { + position: "absolute" + }, + reference: {} + }; + Object.assign(state.elements.popper.style, initialStyles.popper); + state.styles = initialStyles; + if (state.elements.arrow) { + Object.assign(state.elements.arrow.style, initialStyles.arrow); + } + return function() { + Object.keys(state.elements).forEach(function(name) { + var element = state.elements[name]; + var attributes = state.attributes[name] || {}; + var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); + var style = styleProperties.reduce(function(style2, property) { + style2[property] = ""; + return style2; + }, {}); + if (!isHTMLElement(element) || !getNodeName(element)) { + return; + } + Object.assign(element.style, style); + Object.keys(attributes).forEach(function(attribute) { + element.removeAttribute(attribute); + }); + }); + }; +} +var applyStyles_default = { + name: "applyStyles", + enabled: true, + phase: "write", + fn: applyStyles, + effect, + requires: ["computeStyles"] +}; + +// node_modules/@popperjs/core/lib/utils/getBasePlacement.js +function getBasePlacement(placement) { + return placement.split("-")[0]; +} + +// node_modules/@popperjs/core/lib/utils/math.js +var max = Math.max; +var min = Math.min; +var round = Math.round; + +// node_modules/@popperjs/core/lib/utils/userAgent.js +function getUAString() { + var uaData = navigator.userAgentData; + if (uaData != null && uaData.brands) { + return uaData.brands.map(function(item) { + return item.brand + "/" + item.version; + }).join(" "); + } + return navigator.userAgent; +} + +// node_modules/@popperjs/core/lib/dom-utils/isLayoutViewport.js +function isLayoutViewport() { + return !/^((?!chrome|android).)*safari/i.test(getUAString()); +} + +// node_modules/@popperjs/core/lib/dom-utils/getBoundingClientRect.js +function getBoundingClientRect(element, includeScale, isFixedStrategy) { + if (includeScale === void 0) { + includeScale = false; + } + if (isFixedStrategy === void 0) { + isFixedStrategy = false; + } + var clientRect = element.getBoundingClientRect(); + var scaleX = 1; + var scaleY = 1; + if (includeScale && isHTMLElement(element)) { + scaleX = element.offsetWidth > 0 ? round(clientRect.width) / element.offsetWidth || 1 : 1; + scaleY = element.offsetHeight > 0 ? round(clientRect.height) / element.offsetHeight || 1 : 1; + } + var _ref = isElement(element) ? getWindow(element) : window, visualViewport = _ref.visualViewport; + var addVisualOffsets = !isLayoutViewport() && isFixedStrategy; + var x = (clientRect.left + (addVisualOffsets && visualViewport ? visualViewport.offsetLeft : 0)) / scaleX; + var y = (clientRect.top + (addVisualOffsets && visualViewport ? visualViewport.offsetTop : 0)) / scaleY; + var width = clientRect.width / scaleX; + var height = clientRect.height / scaleY; + return { + width, + height, + top: y, + right: x + width, + bottom: y + height, + left: x, + x, + y + }; +} + +// node_modules/@popperjs/core/lib/dom-utils/getLayoutRect.js +function getLayoutRect(element) { + var clientRect = getBoundingClientRect(element); + var width = element.offsetWidth; + var height = element.offsetHeight; + if (Math.abs(clientRect.width - width) <= 1) { + width = clientRect.width; + } + if (Math.abs(clientRect.height - height) <= 1) { + height = clientRect.height; + } + return { + x: element.offsetLeft, + y: element.offsetTop, + width, + height + }; +} + +// node_modules/@popperjs/core/lib/dom-utils/contains.js +function contains(parent, child) { + var rootNode = child.getRootNode && child.getRootNode(); + if (parent.contains(child)) { + return true; + } else if (rootNode && isShadowRoot(rootNode)) { + var next = child; + do { + if (next && parent.isSameNode(next)) { + return true; + } + next = next.parentNode || next.host; + } while (next); + } + return false; +} + +// node_modules/@popperjs/core/lib/dom-utils/getComputedStyle.js +function getComputedStyle(element) { + return getWindow(element).getComputedStyle(element); +} + +// node_modules/@popperjs/core/lib/dom-utils/isTableElement.js +function isTableElement(element) { + return ["table", "td", "th"].indexOf(getNodeName(element)) >= 0; +} + +// node_modules/@popperjs/core/lib/dom-utils/getDocumentElement.js +function getDocumentElement(element) { + return ((isElement(element) ? element.ownerDocument : element.document) || window.document).documentElement; +} + +// node_modules/@popperjs/core/lib/dom-utils/getParentNode.js +function getParentNode(element) { + if (getNodeName(element) === "html") { + return element; + } + return element.assignedSlot || element.parentNode || (isShadowRoot(element) ? element.host : null) || getDocumentElement(element); +} + +// node_modules/@popperjs/core/lib/dom-utils/getOffsetParent.js +function getTrueOffsetParent(element) { + if (!isHTMLElement(element) || getComputedStyle(element).position === "fixed") { + return null; + } + return element.offsetParent; +} +function getContainingBlock(element) { + var isFirefox = /firefox/i.test(getUAString()); + var isIE = /Trident/i.test(getUAString()); + if (isIE && isHTMLElement(element)) { + var elementCss = getComputedStyle(element); + if (elementCss.position === "fixed") { + return null; + } + } + var currentNode = getParentNode(element); + if (isShadowRoot(currentNode)) { + currentNode = currentNode.host; + } + while (isHTMLElement(currentNode) && ["html", "body"].indexOf(getNodeName(currentNode)) < 0) { + var css = getComputedStyle(currentNode); + if (css.transform !== "none" || css.perspective !== "none" || css.contain === "paint" || ["transform", "perspective"].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === "filter" || isFirefox && css.filter && css.filter !== "none") { + return currentNode; + } else { + currentNode = currentNode.parentNode; + } + } + return null; +} +function getOffsetParent(element) { + var window2 = getWindow(element); + var offsetParent = getTrueOffsetParent(element); + while (offsetParent && isTableElement(offsetParent) && getComputedStyle(offsetParent).position === "static") { + offsetParent = getTrueOffsetParent(offsetParent); + } + if (offsetParent && (getNodeName(offsetParent) === "html" || getNodeName(offsetParent) === "body" && getComputedStyle(offsetParent).position === "static")) { + return window2; + } + return offsetParent || getContainingBlock(element) || window2; +} + +// node_modules/@popperjs/core/lib/utils/getMainAxisFromPlacement.js +function getMainAxisFromPlacement(placement) { + return ["top", "bottom"].indexOf(placement) >= 0 ? "x" : "y"; +} + +// node_modules/@popperjs/core/lib/utils/within.js +function within(min2, value, max2) { + return max(min2, min(value, max2)); +} +function withinMaxClamp(min2, value, max2) { + var v = within(min2, value, max2); + return v > max2 ? max2 : v; +} + +// node_modules/@popperjs/core/lib/utils/getFreshSideObject.js +function getFreshSideObject() { + return { + top: 0, + right: 0, + bottom: 0, + left: 0 + }; +} + +// node_modules/@popperjs/core/lib/utils/mergePaddingObject.js +function mergePaddingObject(paddingObject) { + return Object.assign({}, getFreshSideObject(), paddingObject); +} + +// node_modules/@popperjs/core/lib/utils/expandToHashMap.js +function expandToHashMap(value, keys) { + return keys.reduce(function(hashMap, key) { + hashMap[key] = value; + return hashMap; + }, {}); +} + +// node_modules/@popperjs/core/lib/modifiers/arrow.js +var toPaddingObject = function toPaddingObject2(padding, state) { + padding = typeof padding === "function" ? padding(Object.assign({}, state.rects, { + placement: state.placement + })) : padding; + return mergePaddingObject(typeof padding !== "number" ? padding : expandToHashMap(padding, basePlacements)); +}; +function arrow(_ref) { + var _state$modifiersData$; + var state = _ref.state, name = _ref.name, options = _ref.options; + var arrowElement = state.elements.arrow; + var popperOffsets2 = state.modifiersData.popperOffsets; + var basePlacement = getBasePlacement(state.placement); + var axis = getMainAxisFromPlacement(basePlacement); + var isVertical = [left, right].indexOf(basePlacement) >= 0; + var len = isVertical ? "height" : "width"; + if (!arrowElement || !popperOffsets2) { + return; + } + var paddingObject = toPaddingObject(options.padding, state); + var arrowRect = getLayoutRect(arrowElement); + var minProp = axis === "y" ? top : left; + var maxProp = axis === "y" ? bottom : right; + var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets2[axis] - state.rects.popper[len]; + var startDiff = popperOffsets2[axis] - state.rects.reference[axis]; + var arrowOffsetParent = getOffsetParent(arrowElement); + var clientSize = arrowOffsetParent ? axis === "y" ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0; + var centerToReference = endDiff / 2 - startDiff / 2; + var min2 = paddingObject[minProp]; + var max2 = clientSize - arrowRect[len] - paddingObject[maxProp]; + var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference; + var offset2 = within(min2, center, max2); + var axisProp = axis; + state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset2, _state$modifiersData$.centerOffset = offset2 - center, _state$modifiersData$); +} +function effect2(_ref2) { + var state = _ref2.state, options = _ref2.options; + var _options$element = options.element, arrowElement = _options$element === void 0 ? "[data-popper-arrow]" : _options$element; + if (arrowElement == null) { + return; + } + if (typeof arrowElement === "string") { + arrowElement = state.elements.popper.querySelector(arrowElement); + if (!arrowElement) { + return; + } + } + if (true) { + if (!isHTMLElement(arrowElement)) { + console.error(['Popper: "arrow" element must be an HTMLElement (not an SVGElement).', "To use an SVG arrow, wrap it in an HTMLElement that will be used as", "the arrow."].join(" ")); + } + } + if (!contains(state.elements.popper, arrowElement)) { + if (true) { + console.error(['Popper: "arrow" modifier\'s `element` must be a child of the popper', "element."].join(" ")); + } + return; + } + state.elements.arrow = arrowElement; +} +var arrow_default = { + name: "arrow", + enabled: true, + phase: "main", + fn: arrow, + effect: effect2, + requires: ["popperOffsets"], + requiresIfExists: ["preventOverflow"] +}; + +// node_modules/@popperjs/core/lib/utils/getVariation.js +function getVariation(placement) { + return placement.split("-")[1]; +} + +// node_modules/@popperjs/core/lib/modifiers/computeStyles.js +var unsetSides = { + top: "auto", + right: "auto", + bottom: "auto", + left: "auto" +}; +function roundOffsetsByDPR(_ref) { + var x = _ref.x, y = _ref.y; + var win = window; + var dpr = win.devicePixelRatio || 1; + return { + x: round(x * dpr) / dpr || 0, + y: round(y * dpr) / dpr || 0 + }; +} +function mapToStyles(_ref2) { + var _Object$assign2; + var popper2 = _ref2.popper, popperRect = _ref2.popperRect, placement = _ref2.placement, variation = _ref2.variation, offsets = _ref2.offsets, position = _ref2.position, gpuAcceleration = _ref2.gpuAcceleration, adaptive = _ref2.adaptive, roundOffsets = _ref2.roundOffsets, isFixed = _ref2.isFixed; + var _offsets$x = offsets.x, x = _offsets$x === void 0 ? 0 : _offsets$x, _offsets$y = offsets.y, y = _offsets$y === void 0 ? 0 : _offsets$y; + var _ref3 = typeof roundOffsets === "function" ? roundOffsets({ + x, + y + }) : { + x, + y + }; + x = _ref3.x; + y = _ref3.y; + var hasX = offsets.hasOwnProperty("x"); + var hasY = offsets.hasOwnProperty("y"); + var sideX = left; + var sideY = top; + var win = window; + if (adaptive) { + var offsetParent = getOffsetParent(popper2); + var heightProp = "clientHeight"; + var widthProp = "clientWidth"; + if (offsetParent === getWindow(popper2)) { + offsetParent = getDocumentElement(popper2); + if (getComputedStyle(offsetParent).position !== "static" && position === "absolute") { + heightProp = "scrollHeight"; + widthProp = "scrollWidth"; + } + } + offsetParent = offsetParent; + if (placement === top || (placement === left || placement === right) && variation === end) { + sideY = bottom; + var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : offsetParent[heightProp]; + y -= offsetY - popperRect.height; + y *= gpuAcceleration ? 1 : -1; + } + if (placement === left || (placement === top || placement === bottom) && variation === end) { + sideX = right; + var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : offsetParent[widthProp]; + x -= offsetX - popperRect.width; + x *= gpuAcceleration ? 1 : -1; + } + } + var commonStyles = Object.assign({ + position + }, adaptive && unsetSides); + var _ref4 = roundOffsets === true ? roundOffsetsByDPR({ + x, + y + }) : { + x, + y + }; + x = _ref4.x; + y = _ref4.y; + if (gpuAcceleration) { + var _Object$assign; + return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? "0" : "", _Object$assign[sideX] = hasX ? "0" : "", _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? "translate(" + x + "px, " + y + "px)" : "translate3d(" + x + "px, " + y + "px, 0)", _Object$assign)); + } + return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + "px" : "", _Object$assign2[sideX] = hasX ? x + "px" : "", _Object$assign2.transform = "", _Object$assign2)); +} +function computeStyles(_ref5) { + var state = _ref5.state, options = _ref5.options; + var _options$gpuAccelerat = options.gpuAcceleration, gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat, _options$adaptive = options.adaptive, adaptive = _options$adaptive === void 0 ? true : _options$adaptive, _options$roundOffsets = options.roundOffsets, roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets; + if (true) { + var transitionProperty = getComputedStyle(state.elements.popper).transitionProperty || ""; + if (adaptive && ["transform", "top", "right", "bottom", "left"].some(function(property) { + return transitionProperty.indexOf(property) >= 0; + })) { + console.warn(["Popper: Detected CSS transitions on at least one of the following", 'CSS properties: "transform", "top", "right", "bottom", "left".', "\n\n", 'Disable the "computeStyles" modifier\'s `adaptive` option to allow', "for smooth transitions, or remove these properties from the CSS", "transition declaration on the popper element if only transitioning", "opacity or background-color for example.", "\n\n", "We recommend using the popper element as a wrapper around an inner", "element that can have any CSS property transitioned for animations."].join(" ")); + } + } + var commonStyles = { + placement: getBasePlacement(state.placement), + variation: getVariation(state.placement), + popper: state.elements.popper, + popperRect: state.rects.popper, + gpuAcceleration, + isFixed: state.options.strategy === "fixed" + }; + if (state.modifiersData.popperOffsets != null) { + state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.popperOffsets, + position: state.options.strategy, + adaptive, + roundOffsets + }))); + } + if (state.modifiersData.arrow != null) { + state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, { + offsets: state.modifiersData.arrow, + position: "absolute", + adaptive: false, + roundOffsets + }))); + } + state.attributes.popper = Object.assign({}, state.attributes.popper, { + "data-popper-placement": state.placement + }); +} +var computeStyles_default = { + name: "computeStyles", + enabled: true, + phase: "beforeWrite", + fn: computeStyles, + data: {} +}; + +// node_modules/@popperjs/core/lib/modifiers/eventListeners.js +var passive = { + passive: true +}; +function effect3(_ref) { + var state = _ref.state, instance = _ref.instance, options = _ref.options; + var _options$scroll = options.scroll, scroll = _options$scroll === void 0 ? true : _options$scroll, _options$resize = options.resize, resize = _options$resize === void 0 ? true : _options$resize; + var window2 = getWindow(state.elements.popper); + var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper); + if (scroll) { + scrollParents.forEach(function(scrollParent) { + scrollParent.addEventListener("scroll", instance.update, passive); + }); + } + if (resize) { + window2.addEventListener("resize", instance.update, passive); + } + return function() { + if (scroll) { + scrollParents.forEach(function(scrollParent) { + scrollParent.removeEventListener("scroll", instance.update, passive); + }); + } + if (resize) { + window2.removeEventListener("resize", instance.update, passive); + } + }; +} +var eventListeners_default = { + name: "eventListeners", + enabled: true, + phase: "write", + fn: function fn() { + }, + effect: effect3, + data: {} +}; + +// node_modules/@popperjs/core/lib/utils/getOppositePlacement.js +var hash = { + left: "right", + right: "left", + bottom: "top", + top: "bottom" +}; +function getOppositePlacement(placement) { + return placement.replace(/left|right|bottom|top/g, function(matched) { + return hash[matched]; + }); +} + +// node_modules/@popperjs/core/lib/utils/getOppositeVariationPlacement.js +var hash2 = { + start: "end", + end: "start" +}; +function getOppositeVariationPlacement(placement) { + return placement.replace(/start|end/g, function(matched) { + return hash2[matched]; + }); +} + +// node_modules/@popperjs/core/lib/dom-utils/getWindowScroll.js +function getWindowScroll(node) { + var win = getWindow(node); + var scrollLeft = win.pageXOffset; + var scrollTop = win.pageYOffset; + return { + scrollLeft, + scrollTop + }; +} + +// node_modules/@popperjs/core/lib/dom-utils/getWindowScrollBarX.js +function getWindowScrollBarX(element) { + return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft; +} + +// node_modules/@popperjs/core/lib/dom-utils/getViewportRect.js +function getViewportRect(element, strategy) { + var win = getWindow(element); + var html = getDocumentElement(element); + var visualViewport = win.visualViewport; + var width = html.clientWidth; + var height = html.clientHeight; + var x = 0; + var y = 0; + if (visualViewport) { + width = visualViewport.width; + height = visualViewport.height; + var layoutViewport = isLayoutViewport(); + if (layoutViewport || !layoutViewport && strategy === "fixed") { + x = visualViewport.offsetLeft; + y = visualViewport.offsetTop; + } + } + return { + width, + height, + x: x + getWindowScrollBarX(element), + y + }; +} + +// node_modules/@popperjs/core/lib/dom-utils/getDocumentRect.js +function getDocumentRect(element) { + var _element$ownerDocumen; + var html = getDocumentElement(element); + var winScroll = getWindowScroll(element); + var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body; + var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0); + var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0); + var x = -winScroll.scrollLeft + getWindowScrollBarX(element); + var y = -winScroll.scrollTop; + if (getComputedStyle(body || html).direction === "rtl") { + x += max(html.clientWidth, body ? body.clientWidth : 0) - width; + } + return { + width, + height, + x, + y + }; +} + +// node_modules/@popperjs/core/lib/dom-utils/isScrollParent.js +function isScrollParent(element) { + var _getComputedStyle = getComputedStyle(element), overflow = _getComputedStyle.overflow, overflowX = _getComputedStyle.overflowX, overflowY = _getComputedStyle.overflowY; + return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX); +} + +// node_modules/@popperjs/core/lib/dom-utils/getScrollParent.js +function getScrollParent(node) { + if (["html", "body", "#document"].indexOf(getNodeName(node)) >= 0) { + return node.ownerDocument.body; + } + if (isHTMLElement(node) && isScrollParent(node)) { + return node; + } + return getScrollParent(getParentNode(node)); +} + +// node_modules/@popperjs/core/lib/dom-utils/listScrollParents.js +function listScrollParents(element, list) { + var _element$ownerDocumen; + if (list === void 0) { + list = []; + } + var scrollParent = getScrollParent(element); + var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body); + var win = getWindow(scrollParent); + var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent; + var updatedList = list.concat(target); + return isBody ? updatedList : updatedList.concat(listScrollParents(getParentNode(target))); +} + +// node_modules/@popperjs/core/lib/utils/rectToClientRect.js +function rectToClientRect(rect) { + return Object.assign({}, rect, { + left: rect.x, + top: rect.y, + right: rect.x + rect.width, + bottom: rect.y + rect.height + }); +} + +// node_modules/@popperjs/core/lib/dom-utils/getClippingRect.js +function getInnerBoundingClientRect(element, strategy) { + var rect = getBoundingClientRect(element, false, strategy === "fixed"); + rect.top = rect.top + element.clientTop; + rect.left = rect.left + element.clientLeft; + rect.bottom = rect.top + element.clientHeight; + rect.right = rect.left + element.clientWidth; + rect.width = element.clientWidth; + rect.height = element.clientHeight; + rect.x = rect.left; + rect.y = rect.top; + return rect; +} +function getClientRectFromMixedType(element, clippingParent, strategy) { + return clippingParent === viewport ? rectToClientRect(getViewportRect(element, strategy)) : isElement(clippingParent) ? getInnerBoundingClientRect(clippingParent, strategy) : rectToClientRect(getDocumentRect(getDocumentElement(element))); +} +function getClippingParents(element) { + var clippingParents2 = listScrollParents(getParentNode(element)); + var canEscapeClipping = ["absolute", "fixed"].indexOf(getComputedStyle(element).position) >= 0; + var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element; + if (!isElement(clipperElement)) { + return []; + } + return clippingParents2.filter(function(clippingParent) { + return isElement(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== "body"; + }); +} +function getClippingRect(element, boundary, rootBoundary, strategy) { + var mainClippingParents = boundary === "clippingParents" ? getClippingParents(element) : [].concat(boundary); + var clippingParents2 = [].concat(mainClippingParents, [rootBoundary]); + var firstClippingParent = clippingParents2[0]; + var clippingRect = clippingParents2.reduce(function(accRect, clippingParent) { + var rect = getClientRectFromMixedType(element, clippingParent, strategy); + accRect.top = max(rect.top, accRect.top); + accRect.right = min(rect.right, accRect.right); + accRect.bottom = min(rect.bottom, accRect.bottom); + accRect.left = max(rect.left, accRect.left); + return accRect; + }, getClientRectFromMixedType(element, firstClippingParent, strategy)); + clippingRect.width = clippingRect.right - clippingRect.left; + clippingRect.height = clippingRect.bottom - clippingRect.top; + clippingRect.x = clippingRect.left; + clippingRect.y = clippingRect.top; + return clippingRect; +} + +// node_modules/@popperjs/core/lib/utils/computeOffsets.js +function computeOffsets(_ref) { + var reference2 = _ref.reference, element = _ref.element, placement = _ref.placement; + var basePlacement = placement ? getBasePlacement(placement) : null; + var variation = placement ? getVariation(placement) : null; + var commonX = reference2.x + reference2.width / 2 - element.width / 2; + var commonY = reference2.y + reference2.height / 2 - element.height / 2; + var offsets; + switch (basePlacement) { + case top: + offsets = { + x: commonX, + y: reference2.y - element.height + }; + break; + case bottom: + offsets = { + x: commonX, + y: reference2.y + reference2.height + }; + break; + case right: + offsets = { + x: reference2.x + reference2.width, + y: commonY + }; + break; + case left: + offsets = { + x: reference2.x - element.width, + y: commonY + }; + break; + default: + offsets = { + x: reference2.x, + y: reference2.y + }; + } + var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null; + if (mainAxis != null) { + var len = mainAxis === "y" ? "height" : "width"; + switch (variation) { + case start: + offsets[mainAxis] = offsets[mainAxis] - (reference2[len] / 2 - element[len] / 2); + break; + case end: + offsets[mainAxis] = offsets[mainAxis] + (reference2[len] / 2 - element[len] / 2); + break; + default: + } + } + return offsets; +} + +// node_modules/@popperjs/core/lib/utils/detectOverflow.js +function detectOverflow(state, options) { + if (options === void 0) { + options = {}; + } + var _options = options, _options$placement = _options.placement, placement = _options$placement === void 0 ? state.placement : _options$placement, _options$strategy = _options.strategy, strategy = _options$strategy === void 0 ? state.strategy : _options$strategy, _options$boundary = _options.boundary, boundary = _options$boundary === void 0 ? clippingParents : _options$boundary, _options$rootBoundary = _options.rootBoundary, rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary, _options$elementConte = _options.elementContext, elementContext = _options$elementConte === void 0 ? popper : _options$elementConte, _options$altBoundary = _options.altBoundary, altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary, _options$padding = _options.padding, padding = _options$padding === void 0 ? 0 : _options$padding; + var paddingObject = mergePaddingObject(typeof padding !== "number" ? padding : expandToHashMap(padding, basePlacements)); + var altContext = elementContext === popper ? reference : popper; + var popperRect = state.rects.popper; + var element = state.elements[altBoundary ? altContext : elementContext]; + var clippingClientRect = getClippingRect(isElement(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary, strategy); + var referenceClientRect = getBoundingClientRect(state.elements.reference); + var popperOffsets2 = computeOffsets({ + reference: referenceClientRect, + element: popperRect, + strategy: "absolute", + placement + }); + var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets2)); + var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; + var overflowOffsets = { + top: clippingClientRect.top - elementClientRect.top + paddingObject.top, + bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom, + left: clippingClientRect.left - elementClientRect.left + paddingObject.left, + right: elementClientRect.right - clippingClientRect.right + paddingObject.right + }; + var offsetData = state.modifiersData.offset; + if (elementContext === popper && offsetData) { + var offset2 = offsetData[placement]; + Object.keys(overflowOffsets).forEach(function(key) { + var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1; + var axis = [top, bottom].indexOf(key) >= 0 ? "y" : "x"; + overflowOffsets[key] += offset2[axis] * multiply; + }); + } + return overflowOffsets; +} + +// node_modules/@popperjs/core/lib/utils/computeAutoPlacement.js +function computeAutoPlacement(state, options) { + if (options === void 0) { + options = {}; + } + var _options = options, placement = _options.placement, boundary = _options.boundary, rootBoundary = _options.rootBoundary, padding = _options.padding, flipVariations = _options.flipVariations, _options$allowedAutoP = _options.allowedAutoPlacements, allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP; + var variation = getVariation(placement); + var placements2 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function(placement2) { + return getVariation(placement2) === variation; + }) : basePlacements; + var allowedPlacements = placements2.filter(function(placement2) { + return allowedAutoPlacements.indexOf(placement2) >= 0; + }); + if (allowedPlacements.length === 0) { + allowedPlacements = placements2; + if (true) { + console.error(["Popper: The `allowedAutoPlacements` option did not allow any", "placements. Ensure the `placement` option matches the variation", "of the allowed placements.", 'For example, "auto" cannot be used to allow "bottom-start".', 'Use "auto-start" instead.'].join(" ")); + } + } + var overflows = allowedPlacements.reduce(function(acc, placement2) { + acc[placement2] = detectOverflow(state, { + placement: placement2, + boundary, + rootBoundary, + padding + })[getBasePlacement(placement2)]; + return acc; + }, {}); + return Object.keys(overflows).sort(function(a, b) { + return overflows[a] - overflows[b]; + }); +} + +// node_modules/@popperjs/core/lib/modifiers/flip.js +function getExpandedFallbackPlacements(placement) { + if (getBasePlacement(placement) === auto) { + return []; + } + var oppositePlacement = getOppositePlacement(placement); + return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)]; +} +function flip(_ref) { + var state = _ref.state, options = _ref.options, name = _ref.name; + if (state.modifiersData[name]._skip) { + return; + } + var _options$mainAxis = options.mainAxis, checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, _options$altAxis = options.altAxis, checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis, specifiedFallbackPlacements = options.fallbackPlacements, padding = options.padding, boundary = options.boundary, rootBoundary = options.rootBoundary, altBoundary = options.altBoundary, _options$flipVariatio = options.flipVariations, flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio, allowedAutoPlacements = options.allowedAutoPlacements; + var preferredPlacement = state.options.placement; + var basePlacement = getBasePlacement(preferredPlacement); + var isBasePlacement = basePlacement === preferredPlacement; + var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement)); + var placements2 = [preferredPlacement].concat(fallbackPlacements).reduce(function(acc, placement2) { + return acc.concat(getBasePlacement(placement2) === auto ? computeAutoPlacement(state, { + placement: placement2, + boundary, + rootBoundary, + padding, + flipVariations, + allowedAutoPlacements + }) : placement2); + }, []); + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var checksMap = new Map(); + var makeFallbackChecks = true; + var firstFittingPlacement = placements2[0]; + for (var i = 0; i < placements2.length; i++) { + var placement = placements2[i]; + var _basePlacement = getBasePlacement(placement); + var isStartVariation = getVariation(placement) === start; + var isVertical = [top, bottom].indexOf(_basePlacement) >= 0; + var len = isVertical ? "width" : "height"; + var overflow = detectOverflow(state, { + placement, + boundary, + rootBoundary, + altBoundary, + padding + }); + var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top; + if (referenceRect[len] > popperRect[len]) { + mainVariationSide = getOppositePlacement(mainVariationSide); + } + var altVariationSide = getOppositePlacement(mainVariationSide); + var checks = []; + if (checkMainAxis) { + checks.push(overflow[_basePlacement] <= 0); + } + if (checkAltAxis) { + checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0); + } + if (checks.every(function(check) { + return check; + })) { + firstFittingPlacement = placement; + makeFallbackChecks = false; + break; + } + checksMap.set(placement, checks); + } + if (makeFallbackChecks) { + var numberOfChecks = flipVariations ? 3 : 1; + var _loop = function _loop2(_i2) { + var fittingPlacement = placements2.find(function(placement2) { + var checks2 = checksMap.get(placement2); + if (checks2) { + return checks2.slice(0, _i2).every(function(check) { + return check; + }); + } + }); + if (fittingPlacement) { + firstFittingPlacement = fittingPlacement; + return "break"; + } + }; + for (var _i = numberOfChecks; _i > 0; _i--) { + var _ret = _loop(_i); + if (_ret === "break") + break; + } + } + if (state.placement !== firstFittingPlacement) { + state.modifiersData[name]._skip = true; + state.placement = firstFittingPlacement; + state.reset = true; + } +} +var flip_default = { + name: "flip", + enabled: true, + phase: "main", + fn: flip, + requiresIfExists: ["offset"], + data: { + _skip: false + } +}; + +// node_modules/@popperjs/core/lib/modifiers/hide.js +function getSideOffsets(overflow, rect, preventedOffsets) { + if (preventedOffsets === void 0) { + preventedOffsets = { + x: 0, + y: 0 + }; + } + return { + top: overflow.top - rect.height - preventedOffsets.y, + right: overflow.right - rect.width + preventedOffsets.x, + bottom: overflow.bottom - rect.height + preventedOffsets.y, + left: overflow.left - rect.width - preventedOffsets.x + }; +} +function isAnySideFullyClipped(overflow) { + return [top, right, bottom, left].some(function(side) { + return overflow[side] >= 0; + }); +} +function hide(_ref) { + var state = _ref.state, name = _ref.name; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var preventedOffsets = state.modifiersData.preventOverflow; + var referenceOverflow = detectOverflow(state, { + elementContext: "reference" + }); + var popperAltOverflow = detectOverflow(state, { + altBoundary: true + }); + var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect); + var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets); + var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets); + var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets); + state.modifiersData[name] = { + referenceClippingOffsets, + popperEscapeOffsets, + isReferenceHidden, + hasPopperEscaped + }; + state.attributes.popper = Object.assign({}, state.attributes.popper, { + "data-popper-reference-hidden": isReferenceHidden, + "data-popper-escaped": hasPopperEscaped + }); +} +var hide_default = { + name: "hide", + enabled: true, + phase: "main", + requiresIfExists: ["preventOverflow"], + fn: hide +}; + +// node_modules/@popperjs/core/lib/modifiers/offset.js +function distanceAndSkiddingToXY(placement, rects, offset2) { + var basePlacement = getBasePlacement(placement); + var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1; + var _ref = typeof offset2 === "function" ? offset2(Object.assign({}, rects, { + placement + })) : offset2, skidding = _ref[0], distance = _ref[1]; + skidding = skidding || 0; + distance = (distance || 0) * invertDistance; + return [left, right].indexOf(basePlacement) >= 0 ? { + x: distance, + y: skidding + } : { + x: skidding, + y: distance + }; +} +function offset(_ref2) { + var state = _ref2.state, options = _ref2.options, name = _ref2.name; + var _options$offset = options.offset, offset2 = _options$offset === void 0 ? [0, 0] : _options$offset; + var data = placements.reduce(function(acc, placement) { + acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset2); + return acc; + }, {}); + var _data$state$placement = data[state.placement], x = _data$state$placement.x, y = _data$state$placement.y; + if (state.modifiersData.popperOffsets != null) { + state.modifiersData.popperOffsets.x += x; + state.modifiersData.popperOffsets.y += y; + } + state.modifiersData[name] = data; +} +var offset_default = { + name: "offset", + enabled: true, + phase: "main", + requires: ["popperOffsets"], + fn: offset +}; + +// node_modules/@popperjs/core/lib/modifiers/popperOffsets.js +function popperOffsets(_ref) { + var state = _ref.state, name = _ref.name; + state.modifiersData[name] = computeOffsets({ + reference: state.rects.reference, + element: state.rects.popper, + strategy: "absolute", + placement: state.placement + }); +} +var popperOffsets_default = { + name: "popperOffsets", + enabled: true, + phase: "read", + fn: popperOffsets, + data: {} +}; + +// node_modules/@popperjs/core/lib/utils/getAltAxis.js +function getAltAxis(axis) { + return axis === "x" ? "y" : "x"; +} + +// node_modules/@popperjs/core/lib/modifiers/preventOverflow.js +function preventOverflow(_ref) { + var state = _ref.state, options = _ref.options, name = _ref.name; + var _options$mainAxis = options.mainAxis, checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, _options$altAxis = options.altAxis, checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis, boundary = options.boundary, rootBoundary = options.rootBoundary, altBoundary = options.altBoundary, padding = options.padding, _options$tether = options.tether, tether = _options$tether === void 0 ? true : _options$tether, _options$tetherOffset = options.tetherOffset, tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset; + var overflow = detectOverflow(state, { + boundary, + rootBoundary, + padding, + altBoundary + }); + var basePlacement = getBasePlacement(state.placement); + var variation = getVariation(state.placement); + var isBasePlacement = !variation; + var mainAxis = getMainAxisFromPlacement(basePlacement); + var altAxis = getAltAxis(mainAxis); + var popperOffsets2 = state.modifiersData.popperOffsets; + var referenceRect = state.rects.reference; + var popperRect = state.rects.popper; + var tetherOffsetValue = typeof tetherOffset === "function" ? tetherOffset(Object.assign({}, state.rects, { + placement: state.placement + })) : tetherOffset; + var normalizedTetherOffsetValue = typeof tetherOffsetValue === "number" ? { + mainAxis: tetherOffsetValue, + altAxis: tetherOffsetValue + } : Object.assign({ + mainAxis: 0, + altAxis: 0 + }, tetherOffsetValue); + var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null; + var data = { + x: 0, + y: 0 + }; + if (!popperOffsets2) { + return; + } + if (checkMainAxis) { + var _offsetModifierState$; + var mainSide = mainAxis === "y" ? top : left; + var altSide = mainAxis === "y" ? bottom : right; + var len = mainAxis === "y" ? "height" : "width"; + var offset2 = popperOffsets2[mainAxis]; + var min2 = offset2 + overflow[mainSide]; + var max2 = offset2 - overflow[altSide]; + var additive = tether ? -popperRect[len] / 2 : 0; + var minLen = variation === start ? referenceRect[len] : popperRect[len]; + var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; + var arrowElement = state.elements.arrow; + var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : { + width: 0, + height: 0 + }; + var arrowPaddingObject = state.modifiersData["arrow#persistent"] ? state.modifiersData["arrow#persistent"].padding : getFreshSideObject(); + var arrowPaddingMin = arrowPaddingObject[mainSide]; + var arrowPaddingMax = arrowPaddingObject[altSide]; + var arrowLen = within(0, referenceRect[len], arrowRect[len]); + var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis; + var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis; + var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow); + var clientOffset = arrowOffsetParent ? mainAxis === "y" ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0; + var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0; + var tetherMin = offset2 + minOffset - offsetModifierValue - clientOffset; + var tetherMax = offset2 + maxOffset - offsetModifierValue; + var preventedOffset = within(tether ? min(min2, tetherMin) : min2, offset2, tether ? max(max2, tetherMax) : max2); + popperOffsets2[mainAxis] = preventedOffset; + data[mainAxis] = preventedOffset - offset2; + } + if (checkAltAxis) { + var _offsetModifierState$2; + var _mainSide = mainAxis === "x" ? top : left; + var _altSide = mainAxis === "x" ? bottom : right; + var _offset = popperOffsets2[altAxis]; + var _len = altAxis === "y" ? "height" : "width"; + var _min = _offset + overflow[_mainSide]; + var _max = _offset - overflow[_altSide]; + var isOriginSide = [top, left].indexOf(basePlacement) !== -1; + var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0; + var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis; + var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max; + var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max); + popperOffsets2[altAxis] = _preventedOffset; + data[altAxis] = _preventedOffset - _offset; + } + state.modifiersData[name] = data; +} +var preventOverflow_default = { + name: "preventOverflow", + enabled: true, + phase: "main", + fn: preventOverflow, + requiresIfExists: ["offset"] +}; + +// node_modules/@popperjs/core/lib/dom-utils/getHTMLElementScroll.js +function getHTMLElementScroll(element) { + return { + scrollLeft: element.scrollLeft, + scrollTop: element.scrollTop + }; +} + +// node_modules/@popperjs/core/lib/dom-utils/getNodeScroll.js +function getNodeScroll(node) { + if (node === getWindow(node) || !isHTMLElement(node)) { + return getWindowScroll(node); + } else { + return getHTMLElementScroll(node); + } +} + +// node_modules/@popperjs/core/lib/dom-utils/getCompositeRect.js +function isElementScaled(element) { + var rect = element.getBoundingClientRect(); + var scaleX = round(rect.width) / element.offsetWidth || 1; + var scaleY = round(rect.height) / element.offsetHeight || 1; + return scaleX !== 1 || scaleY !== 1; +} +function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) { + if (isFixed === void 0) { + isFixed = false; + } + var isOffsetParentAnElement = isHTMLElement(offsetParent); + var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent); + var documentElement = getDocumentElement(offsetParent); + var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled, isFixed); + var scroll = { + scrollLeft: 0, + scrollTop: 0 + }; + var offsets = { + x: 0, + y: 0 + }; + if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { + if (getNodeName(offsetParent) !== "body" || isScrollParent(documentElement)) { + scroll = getNodeScroll(offsetParent); + } + if (isHTMLElement(offsetParent)) { + offsets = getBoundingClientRect(offsetParent, true); + offsets.x += offsetParent.clientLeft; + offsets.y += offsetParent.clientTop; + } else if (documentElement) { + offsets.x = getWindowScrollBarX(documentElement); + } + } + return { + x: rect.left + scroll.scrollLeft - offsets.x, + y: rect.top + scroll.scrollTop - offsets.y, + width: rect.width, + height: rect.height + }; +} + +// node_modules/@popperjs/core/lib/utils/orderModifiers.js +function order(modifiers) { + var map = new Map(); + var visited = new Set(); + var result = []; + modifiers.forEach(function(modifier) { + map.set(modifier.name, modifier); + }); + function sort(modifier) { + visited.add(modifier.name); + var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []); + requires.forEach(function(dep) { + if (!visited.has(dep)) { + var depModifier = map.get(dep); + if (depModifier) { + sort(depModifier); + } + } + }); + result.push(modifier); + } + modifiers.forEach(function(modifier) { + if (!visited.has(modifier.name)) { + sort(modifier); + } + }); + return result; +} +function orderModifiers(modifiers) { + var orderedModifiers = order(modifiers); + return modifierPhases.reduce(function(acc, phase) { + return acc.concat(orderedModifiers.filter(function(modifier) { + return modifier.phase === phase; + })); + }, []); +} + +// node_modules/@popperjs/core/lib/utils/debounce.js +function debounce(fn2) { + var pending; + return function() { + if (!pending) { + pending = new Promise(function(resolve) { + Promise.resolve().then(function() { + pending = void 0; + resolve(fn2()); + }); + }); + } + return pending; + }; +} + +// node_modules/@popperjs/core/lib/utils/format.js +function format(str) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + return [].concat(args).reduce(function(p, c) { + return p.replace(/%s/, c); + }, str); +} + +// node_modules/@popperjs/core/lib/utils/validateModifiers.js +var INVALID_MODIFIER_ERROR = 'Popper: modifier "%s" provided an invalid %s property, expected %s but got %s'; +var MISSING_DEPENDENCY_ERROR = 'Popper: modifier "%s" requires "%s", but "%s" modifier is not available'; +var VALID_PROPERTIES = ["name", "enabled", "phase", "fn", "effect", "requires", "options"]; +function validateModifiers(modifiers) { + modifiers.forEach(function(modifier) { + [].concat(Object.keys(modifier), VALID_PROPERTIES).filter(function(value, index, self) { + return self.indexOf(value) === index; + }).forEach(function(key) { + switch (key) { + case "name": + if (typeof modifier.name !== "string") { + console.error(format(INVALID_MODIFIER_ERROR, String(modifier.name), '"name"', '"string"', '"' + String(modifier.name) + '"')); + } + break; + case "enabled": + if (typeof modifier.enabled !== "boolean") { + console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"enabled"', '"boolean"', '"' + String(modifier.enabled) + '"')); + } + break; + case "phase": + if (modifierPhases.indexOf(modifier.phase) < 0) { + console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"phase"', "either " + modifierPhases.join(", "), '"' + String(modifier.phase) + '"')); + } + break; + case "fn": + if (typeof modifier.fn !== "function") { + console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"fn"', '"function"', '"' + String(modifier.fn) + '"')); + } + break; + case "effect": + if (modifier.effect != null && typeof modifier.effect !== "function") { + console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"effect"', '"function"', '"' + String(modifier.fn) + '"')); + } + break; + case "requires": + if (modifier.requires != null && !Array.isArray(modifier.requires)) { + console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"requires"', '"array"', '"' + String(modifier.requires) + '"')); + } + break; + case "requiresIfExists": + if (!Array.isArray(modifier.requiresIfExists)) { + console.error(format(INVALID_MODIFIER_ERROR, modifier.name, '"requiresIfExists"', '"array"', '"' + String(modifier.requiresIfExists) + '"')); + } + break; + case "options": + case "data": + break; + default: + console.error('PopperJS: an invalid property has been provided to the "' + modifier.name + '" modifier, valid properties are ' + VALID_PROPERTIES.map(function(s) { + return '"' + s + '"'; + }).join(", ") + '; but "' + key + '" was provided.'); + } + modifier.requires && modifier.requires.forEach(function(requirement) { + if (modifiers.find(function(mod) { + return mod.name === requirement; + }) == null) { + console.error(format(MISSING_DEPENDENCY_ERROR, String(modifier.name), requirement, requirement)); + } + }); + }); + }); +} + +// node_modules/@popperjs/core/lib/utils/uniqueBy.js +function uniqueBy(arr, fn2) { + var identifiers = new Set(); + return arr.filter(function(item) { + var identifier = fn2(item); + if (!identifiers.has(identifier)) { + identifiers.add(identifier); + return true; + } + }); +} + +// node_modules/@popperjs/core/lib/utils/mergeByName.js +function mergeByName(modifiers) { + var merged = modifiers.reduce(function(merged2, current) { + var existing = merged2[current.name]; + merged2[current.name] = existing ? Object.assign({}, existing, current, { + options: Object.assign({}, existing.options, current.options), + data: Object.assign({}, existing.data, current.data) + }) : current; + return merged2; + }, {}); + return Object.keys(merged).map(function(key) { + return merged[key]; + }); +} + +// node_modules/@popperjs/core/lib/createPopper.js +var INVALID_ELEMENT_ERROR = "Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element."; +var INFINITE_LOOP_ERROR = "Popper: An infinite loop in the modifiers cycle has been detected! The cycle has been interrupted to prevent a browser crash."; +var DEFAULT_OPTIONS = { + placement: "bottom", + modifiers: [], + strategy: "absolute" +}; +function areValidElements() { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + return !args.some(function(element) { + return !(element && typeof element.getBoundingClientRect === "function"); + }); +} +function popperGenerator(generatorOptions) { + if (generatorOptions === void 0) { + generatorOptions = {}; + } + var _generatorOptions = generatorOptions, _generatorOptions$def = _generatorOptions.defaultModifiers, defaultModifiers2 = _generatorOptions$def === void 0 ? [] : _generatorOptions$def, _generatorOptions$def2 = _generatorOptions.defaultOptions, defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2; + return function createPopper2(reference2, popper2, options) { + if (options === void 0) { + options = defaultOptions; + } + var state = { + placement: "bottom", + orderedModifiers: [], + options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions), + modifiersData: {}, + elements: { + reference: reference2, + popper: popper2 + }, + attributes: {}, + styles: {} + }; + var effectCleanupFns = []; + var isDestroyed = false; + var instance = { + state, + setOptions: function setOptions(setOptionsAction) { + var options2 = typeof setOptionsAction === "function" ? setOptionsAction(state.options) : setOptionsAction; + cleanupModifierEffects(); + state.options = Object.assign({}, defaultOptions, state.options, options2); + state.scrollParents = { + reference: isElement(reference2) ? listScrollParents(reference2) : reference2.contextElement ? listScrollParents(reference2.contextElement) : [], + popper: listScrollParents(popper2) + }; + var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers2, state.options.modifiers))); + state.orderedModifiers = orderedModifiers.filter(function(m) { + return m.enabled; + }); + if (true) { + var modifiers = uniqueBy([].concat(orderedModifiers, state.options.modifiers), function(_ref) { + var name = _ref.name; + return name; + }); + validateModifiers(modifiers); + if (getBasePlacement(state.options.placement) === auto) { + var flipModifier = state.orderedModifiers.find(function(_ref2) { + var name = _ref2.name; + return name === "flip"; + }); + if (!flipModifier) { + console.error(['Popper: "auto" placements require the "flip" modifier be', "present and enabled to work."].join(" ")); + } + } + var _getComputedStyle = getComputedStyle(popper2), marginTop = _getComputedStyle.marginTop, marginRight = _getComputedStyle.marginRight, marginBottom = _getComputedStyle.marginBottom, marginLeft = _getComputedStyle.marginLeft; + if ([marginTop, marginRight, marginBottom, marginLeft].some(function(margin) { + return parseFloat(margin); + })) { + console.warn(['Popper: CSS "margin" styles cannot be used to apply padding', "between the popper and its reference element or boundary.", "To replicate margin, use the `offset` modifier, as well as", "the `padding` option in the `preventOverflow` and `flip`", "modifiers."].join(" ")); + } + } + runModifierEffects(); + return instance.update(); + }, + forceUpdate: function forceUpdate() { + if (isDestroyed) { + return; + } + var _state$elements = state.elements, reference3 = _state$elements.reference, popper3 = _state$elements.popper; + if (!areValidElements(reference3, popper3)) { + if (true) { + console.error(INVALID_ELEMENT_ERROR); + } + return; + } + state.rects = { + reference: getCompositeRect(reference3, getOffsetParent(popper3), state.options.strategy === "fixed"), + popper: getLayoutRect(popper3) + }; + state.reset = false; + state.placement = state.options.placement; + state.orderedModifiers.forEach(function(modifier) { + return state.modifiersData[modifier.name] = Object.assign({}, modifier.data); + }); + var __debug_loops__ = 0; + for (var index = 0; index < state.orderedModifiers.length; index++) { + if (true) { + __debug_loops__ += 1; + if (__debug_loops__ > 100) { + console.error(INFINITE_LOOP_ERROR); + break; + } + } + if (state.reset === true) { + state.reset = false; + index = -1; + continue; + } + var _state$orderedModifie = state.orderedModifiers[index], fn2 = _state$orderedModifie.fn, _state$orderedModifie2 = _state$orderedModifie.options, _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, name = _state$orderedModifie.name; + if (typeof fn2 === "function") { + state = fn2({ + state, + options: _options, + name, + instance + }) || state; + } + } + }, + update: debounce(function() { + return new Promise(function(resolve) { + instance.forceUpdate(); + resolve(state); + }); + }), + destroy: function destroy() { + cleanupModifierEffects(); + isDestroyed = true; + } + }; + if (!areValidElements(reference2, popper2)) { + if (true) { + console.error(INVALID_ELEMENT_ERROR); + } + return instance; + } + instance.setOptions(options).then(function(state2) { + if (!isDestroyed && options.onFirstUpdate) { + options.onFirstUpdate(state2); + } + }); + function runModifierEffects() { + state.orderedModifiers.forEach(function(_ref3) { + var name = _ref3.name, _ref3$options = _ref3.options, options2 = _ref3$options === void 0 ? {} : _ref3$options, effect4 = _ref3.effect; + if (typeof effect4 === "function") { + var cleanupFn = effect4({ + state, + name, + instance, + options: options2 + }); + var noopFn = function noopFn2() { + }; + effectCleanupFns.push(cleanupFn || noopFn); + } + }); + } + function cleanupModifierEffects() { + effectCleanupFns.forEach(function(fn2) { + return fn2(); + }); + effectCleanupFns = []; + } + return instance; + }; +} + +// node_modules/@popperjs/core/lib/popper.js +var defaultModifiers = [eventListeners_default, popperOffsets_default, computeStyles_default, applyStyles_default, offset_default, flip_default, preventOverflow_default, arrow_default, hide_default]; +var createPopper = /* @__PURE__ */ popperGenerator({ + defaultModifiers +}); + +// src/settings/suggesters/suggest.ts +var wrapAround = (value, size) => { + return (value % size + size) % size; +}; +var Suggest = class { + constructor(owner, containerEl, scope) { + this.owner = owner; + this.containerEl = containerEl; + containerEl.on("click", ".suggestion-item", this.onSuggestionClick.bind(this)); + containerEl.on("mousemove", ".suggestion-item", this.onSuggestionMouseover.bind(this)); + scope.register([], "ArrowUp", (event) => { + if (!event.isComposing) { + this.setSelectedItem(this.selectedItem - 1, true); + return false; + } + }); + scope.register([], "ArrowDown", (event) => { + if (!event.isComposing) { + this.setSelectedItem(this.selectedItem + 1, true); + return false; + } + }); + scope.register([], "Enter", (event) => { + if (!event.isComposing) { + this.useSelectedItem(event); + return false; + } + }); + } + onSuggestionClick(event, el) { + event.preventDefault(); + const item = this.suggestions.indexOf(el); + this.setSelectedItem(item, false); + this.useSelectedItem(event); + } + onSuggestionMouseover(_event, el) { + const item = this.suggestions.indexOf(el); + this.setSelectedItem(item, false); + } + setSuggestions(values) { + this.containerEl.empty(); + const suggestionEls = []; + values.forEach((value) => { + const suggestionEl = this.containerEl.createDiv("suggestion-item"); + this.owner.renderSuggestion(value, suggestionEl); + suggestionEls.push(suggestionEl); + }); + this.values = values; + this.suggestions = suggestionEls; + this.setSelectedItem(0, false); + } + useSelectedItem(event) { + const currentValue = this.values[this.selectedItem]; + if (currentValue) { + this.owner.selectSuggestion(currentValue, event); + } + } + setSelectedItem(selectedIndex, scrollIntoView) { + const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length); + const prevSelectedSuggestion = this.suggestions[this.selectedItem]; + const selectedSuggestion = this.suggestions[normalizedIndex]; + prevSelectedSuggestion?.removeClass("is-selected"); + selectedSuggestion?.addClass("is-selected"); + this.selectedItem = normalizedIndex; + if (scrollIntoView) { + selectedSuggestion.scrollIntoView(false); + } + } +}; +var TextInputSuggest = class { + constructor(inputEl) { + this.inputEl = inputEl; + this.scope = new import_obsidian2.Scope(); + this.suggestEl = createDiv("suggestion-container"); + const suggestion = this.suggestEl.createDiv("suggestion"); + this.suggest = new Suggest(this, suggestion, this.scope); + this.scope.register([], "Escape", this.close.bind(this)); + this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); + this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); + this.inputEl.addEventListener("blur", this.close.bind(this)); + this.suggestEl.on("mousedown", ".suggestion-container", (event) => { + event.preventDefault(); + }); + } + onInputChanged() { + const inputStr = this.inputEl.value; + const suggestions = this.getSuggestions(inputStr); + if (!suggestions) { + this.close(); + return; + } + if (suggestions.length > 0) { + this.suggest.setSuggestions(suggestions); + this.open(app.dom.appContainerEl, this.inputEl); + } else { + this.close(); + } + } + open(container, inputEl) { + app.keymap.pushScope(this.scope); + container.appendChild(this.suggestEl); + this.popper = createPopper(inputEl, this.suggestEl, { + placement: "bottom-start", + modifiers: [ + { + name: "sameWidth", + enabled: true, + fn: ({ state, instance }) => { + const targetWidth = `${state.rects.reference.width}px`; + if (state.styles.popper.width === targetWidth) { + return; + } + state.styles.popper.width = targetWidth; + instance.update(); + }, + phase: "beforeWrite", + requires: ["computeStyles"] + } + ] + }); + } + close() { + app.keymap.popScope(this.scope); + this.suggest.setSuggestions([]); + if (this.popper) + this.popper.destroy(); + this.suggestEl.detach(); + } +}; + +// src/settings/suggesters/FolderSuggester.ts +var FolderSuggest = class extends TextInputSuggest { + getSuggestions(inputStr) { + const abstractFiles = app.vault.getAllLoadedFiles(); + const folders = []; + const lowerCaseInputStr = inputStr.toLowerCase(); + abstractFiles.forEach((folder) => { + if (folder instanceof import_obsidian3.TFolder && folder.path.toLowerCase().contains(lowerCaseInputStr)) { + folders.push(folder); + } + }); + return folders; + } + renderSuggestion(file, el) { + el.setText(file.path); + } + selectSuggestion(file) { + this.inputEl.value = file.path; + this.inputEl.trigger("input"); + this.close(); + } +}; + +// src/settings/suggesters/FileSuggester.ts +var import_obsidian5 = __toModule(require("obsidian")); + +// src/utils/Utils.ts +var import_obsidian4 = __toModule(require("obsidian")); +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +function escape_RegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} +function generate_dynamic_command_regex() { + return /(<%(?:-|_)?\s*[*~]{0,1})\+((?:.|\s)*?%>)/g; +} +function resolve_tfolder(folder_str) { + folder_str = (0, import_obsidian4.normalizePath)(folder_str); + const folder = app.vault.getAbstractFileByPath(folder_str); + if (!folder) { + throw new TemplaterError(`Folder "${folder_str}" doesn't exist`); + } + if (!(folder instanceof import_obsidian4.TFolder)) { + throw new TemplaterError(`${folder_str} is a file, not a folder`); + } + return folder; +} +function resolve_tfile(file_str) { + file_str = (0, import_obsidian4.normalizePath)(file_str); + const file = app.vault.getAbstractFileByPath(file_str); + if (!file) { + throw new TemplaterError(`File "${file_str}" doesn't exist`); + } + if (!(file instanceof import_obsidian4.TFile)) { + throw new TemplaterError(`${file_str} is a folder, not a file`); + } + return file; +} +function get_tfiles_from_folder(folder_str) { + const folder = resolve_tfolder(folder_str); + const files = []; + import_obsidian4.Vault.recurseChildren(folder, (file) => { + if (file instanceof import_obsidian4.TFile) { + files.push(file); + } + }); + files.sort((a, b) => { + return a.basename.localeCompare(b.basename); + }); + return files; +} +function arraymove(arr, fromIndex, toIndex) { + if (toIndex < 0 || toIndex === arr.length) { + return; + } + const element = arr[fromIndex]; + arr[fromIndex] = arr[toIndex]; + arr[toIndex] = element; +} + +// src/settings/suggesters/FileSuggester.ts +var FileSuggestMode; +(function(FileSuggestMode2) { + FileSuggestMode2[FileSuggestMode2["TemplateFiles"] = 0] = "TemplateFiles"; + FileSuggestMode2[FileSuggestMode2["ScriptFiles"] = 1] = "ScriptFiles"; +})(FileSuggestMode || (FileSuggestMode = {})); +var FileSuggest = class extends TextInputSuggest { + constructor(inputEl, plugin, mode) { + super(inputEl); + this.inputEl = inputEl; + this.plugin = plugin; + this.mode = mode; + } + get_folder(mode) { + switch (mode) { + case 0: + return this.plugin.settings.templates_folder; + case 1: + return this.plugin.settings.user_scripts_folder; + } + } + get_error_msg(mode) { + switch (mode) { + case 0: + return `Templates folder doesn't exist`; + case 1: + return `User Scripts folder doesn't exist`; + } + } + getSuggestions(input_str) { + const all_files = errorWrapperSync(() => get_tfiles_from_folder(this.get_folder(this.mode)), this.get_error_msg(this.mode)); + if (!all_files) { + return []; + } + const files = []; + const lower_input_str = input_str.toLowerCase(); + all_files.forEach((file) => { + if (file instanceof import_obsidian5.TFile && file.extension === "md" && file.path.toLowerCase().contains(lower_input_str)) { + files.push(file); + } + }); + return files; + } + renderSuggestion(file, el) { + el.setText(file.path); + } + selectSuggestion(file) { + this.inputEl.value = file.path; + this.inputEl.trigger("input"); + this.close(); + } +}; + +// src/settings/Settings.ts +var DEFAULT_SETTINGS = { + command_timeout: 5, + templates_folder: "", + templates_pairs: [["", ""]], + trigger_on_file_creation: false, + auto_jump_to_cursor: false, + enable_system_commands: false, + shell_path: "", + user_scripts_folder: "", + enable_folder_templates: true, + folder_templates: [{ folder: "", template: "" }], + syntax_highlighting: true, + enabled_templates_hotkeys: [""], + startup_templates: [""], + enable_ribbon_icon: true +}; +var TemplaterSettingTab = class extends import_obsidian6.PluginSettingTab { + constructor(plugin) { + super(app, plugin); + this.plugin = plugin; + } + display() { + this.containerEl.empty(); + this.add_general_setting_header(); + this.add_template_folder_setting(); + this.add_internal_functions_setting(); + this.add_syntax_highlighting_setting(); + this.add_auto_jump_to_cursor(); + this.add_trigger_on_new_file_creation_setting(); + this.add_ribbon_icon_setting(); + this.add_templates_hotkeys_setting(); + if (this.plugin.settings.trigger_on_file_creation) { + this.add_folder_templates_setting(); + } + this.add_startup_templates_setting(); + this.add_user_script_functions_setting(); + this.add_user_system_command_functions_setting(); + this.add_donating_setting(); + } + add_general_setting_header() { + this.containerEl.createEl("h2", { text: "General Settings" }); + } + add_template_folder_setting() { + new import_obsidian6.Setting(this.containerEl).setName("Template folder location").setDesc("Files in this folder will be available as templates.").addSearch((cb) => { + new FolderSuggest(cb.inputEl); + cb.setPlaceholder("Example: folder1/folder2").setValue(this.plugin.settings.templates_folder).onChange((new_folder) => { + this.plugin.settings.templates_folder = new_folder; + this.plugin.save_settings(); + }); + cb.containerEl.addClass("templater_search"); + }); + } + add_internal_functions_setting() { + const desc = document.createDocumentFragment(); + desc.append("Templater provides multiples predefined variables / functions that you can use.", desc.createEl("br"), "Check the ", desc.createEl("a", { + href: "https://silentvoid13.github.io/Templater/", + text: "documentation" + }), " to get a list of all the available internal variables / functions."); + new import_obsidian6.Setting(this.containerEl).setName("Internal Variables and Functions").setDesc(desc); + } + add_syntax_highlighting_setting() { + const desc = document.createDocumentFragment(); + desc.append("Adds syntax highlighting for Templater commands in edit mode."); + new import_obsidian6.Setting(this.containerEl).setName("Syntax Highlighting").setDesc(desc).addToggle((toggle) => { + toggle.setValue(this.plugin.settings.syntax_highlighting).onChange((syntax_highlighting) => { + this.plugin.settings.syntax_highlighting = syntax_highlighting; + this.plugin.save_settings(); + this.plugin.event_handler.update_syntax_highlighting(); + }); + }); + } + add_auto_jump_to_cursor() { + const desc = document.createDocumentFragment(); + desc.append("Automatically triggers ", desc.createEl("code", { text: "tp.file.cursor" }), " after inserting a template.", desc.createEl("br"), "You can also set a hotkey to manually trigger ", desc.createEl("code", { text: "tp.file.cursor" }), "."); + new import_obsidian6.Setting(this.containerEl).setName("Automatic jump to cursor").setDesc(desc).addToggle((toggle) => { + toggle.setValue(this.plugin.settings.auto_jump_to_cursor).onChange((auto_jump_to_cursor) => { + this.plugin.settings.auto_jump_to_cursor = auto_jump_to_cursor; + this.plugin.save_settings(); + }); + }); + } + add_trigger_on_new_file_creation_setting() { + const desc = document.createDocumentFragment(); + desc.append("Templater will listen for the new file creation event, and replace every command it finds in the new file's content.", desc.createEl("br"), "This makes Templater compatible with other plugins like the Daily note core plugin, Calendar plugin, Review plugin, Note refactor plugin, ...", desc.createEl("br"), desc.createEl("b", { + text: "Warning: " + }), "This can be dangerous if you create new files with unknown / unsafe content on creation. Make sure that every new file's content is safe on creation."); + new import_obsidian6.Setting(this.containerEl).setName("Trigger Templater on new file creation").setDesc(desc).addToggle((toggle) => { + toggle.setValue(this.plugin.settings.trigger_on_file_creation).onChange((trigger_on_file_creation) => { + this.plugin.settings.trigger_on_file_creation = trigger_on_file_creation; + this.plugin.save_settings(); + this.plugin.event_handler.update_trigger_file_on_creation(); + this.display(); + }); + }); + } + add_ribbon_icon_setting() { + const desc = document.createDocumentFragment(); + desc.append("Show Templater icon in sidebar ribbon, allowing you to quickly use templates anywhere."); + new import_obsidian6.Setting(this.containerEl).setName("Show icon in sidebar").setDesc(desc).addToggle((toggle) => { + toggle.setValue(this.plugin.settings.enable_ribbon_icon).onChange((enable_ribbon_icon) => { + this.plugin.settings.enable_ribbon_icon = enable_ribbon_icon; + this.plugin.save_settings(); + if (this.plugin.settings.enable_ribbon_icon) { + this.plugin.addRibbonIcon("templater-icon", "Templater", async () => { + this.plugin.fuzzy_suggester.insert_template(); + }).setAttribute("id", "rb-templater-icon"); + } else { + document.getElementById("rb-templater-icon")?.remove(); + } + }); + }); + } + add_templates_hotkeys_setting() { + this.containerEl.createEl("h2", { text: "Template Hotkeys" }); + const desc = document.createDocumentFragment(); + desc.append("Template Hotkeys allows you to bind a template to a hotkey."); + new import_obsidian6.Setting(this.containerEl).setDesc(desc); + this.plugin.settings.enabled_templates_hotkeys.forEach((template, index) => { + const s = new import_obsidian6.Setting(this.containerEl).addSearch((cb) => { + new FileSuggest(cb.inputEl, this.plugin, FileSuggestMode.TemplateFiles); + cb.setPlaceholder("Example: folder1/template_file").setValue(template).onChange((new_template) => { + if (new_template && this.plugin.settings.enabled_templates_hotkeys.contains(new_template)) { + log_error(new TemplaterError("This template is already bound to a hotkey")); + return; + } + this.plugin.command_handler.add_template_hotkey(this.plugin.settings.enabled_templates_hotkeys[index], new_template); + this.plugin.settings.enabled_templates_hotkeys[index] = new_template; + this.plugin.save_settings(); + }); + cb.containerEl.addClass("templater_search"); + }).addExtraButton((cb) => { + cb.setIcon("any-key").setTooltip("Configure Hotkey").onClick(() => { + app.setting.openTabById("hotkeys"); + const tab = app.setting.activeTab; + tab.searchInputEl.value = "Templater: Insert"; + tab.updateHotkeyVisibility(); + }); + }).addExtraButton((cb) => { + cb.setIcon("up-chevron-glyph").setTooltip("Move up").onClick(() => { + arraymove(this.plugin.settings.enabled_templates_hotkeys, index, index - 1); + this.plugin.save_settings(); + this.display(); + }); + }).addExtraButton((cb) => { + cb.setIcon("down-chevron-glyph").setTooltip("Move down").onClick(() => { + arraymove(this.plugin.settings.enabled_templates_hotkeys, index, index + 1); + this.plugin.save_settings(); + this.display(); + }); + }).addExtraButton((cb) => { + cb.setIcon("cross").setTooltip("Delete").onClick(() => { + this.plugin.command_handler.remove_template_hotkey(this.plugin.settings.enabled_templates_hotkeys[index]); + this.plugin.settings.enabled_templates_hotkeys.splice(index, 1); + this.plugin.save_settings(); + this.display(); + }); + }); + s.infoEl.remove(); + }); + new import_obsidian6.Setting(this.containerEl).addButton((cb) => { + cb.setButtonText("Add new hotkey for template").setCta().onClick(() => { + this.plugin.settings.enabled_templates_hotkeys.push(""); + this.plugin.save_settings(); + this.display(); + }); + }); + } + add_folder_templates_setting() { + this.containerEl.createEl("h2", { text: "Folder Templates" }); + const descHeading = document.createDocumentFragment(); + descHeading.append("Folder Templates are triggered when a new ", descHeading.createEl("strong", { text: "empty " }), "file is created in a given folder.", descHeading.createEl("br"), "Templater will fill the empty file with the specified template.", descHeading.createEl("br"), "The deepest match is used. A global default template would be defined on the root ", descHeading.createEl("code", { text: "/" }), "."); + new import_obsidian6.Setting(this.containerEl).setDesc(descHeading); + const descUseNewFileTemplate = document.createDocumentFragment(); + descUseNewFileTemplate.append("When enabled Templater will make use of the folder templates defined below."); + new import_obsidian6.Setting(this.containerEl).setName("Enable Folder Templates").setDesc(descUseNewFileTemplate).addToggle((toggle) => { + toggle.setValue(this.plugin.settings.enable_folder_templates).onChange((use_new_file_templates) => { + this.plugin.settings.enable_folder_templates = use_new_file_templates; + this.plugin.save_settings(); + this.display(); + }); + }); + if (!this.plugin.settings.enable_folder_templates) { + return; + } + new import_obsidian6.Setting(this.containerEl).setName("Add New").setDesc("Add new folder template").addButton((button) => { + button.setTooltip("Add additional folder template").setButtonText("+").setCta().onClick(() => { + this.plugin.settings.folder_templates.push({ + folder: "", + template: "" + }); + this.plugin.save_settings(); + this.display(); + }); + }); + this.plugin.settings.folder_templates.forEach((folder_template, index) => { + const s = new import_obsidian6.Setting(this.containerEl).addSearch((cb) => { + new FolderSuggest(cb.inputEl); + cb.setPlaceholder("Folder").setValue(folder_template.folder).onChange((new_folder) => { + if (new_folder && this.plugin.settings.folder_templates.some((e) => e.folder == new_folder)) { + log_error(new TemplaterError("This folder already has a template associated with it")); + return; + } + this.plugin.settings.folder_templates[index].folder = new_folder; + this.plugin.save_settings(); + }); + cb.containerEl.addClass("templater_search"); + }).addSearch((cb) => { + new FileSuggest(cb.inputEl, this.plugin, FileSuggestMode.TemplateFiles); + cb.setPlaceholder("Template").setValue(folder_template.template).onChange((new_template) => { + this.plugin.settings.folder_templates[index].template = new_template; + this.plugin.save_settings(); + }); + cb.containerEl.addClass("templater_search"); + }).addExtraButton((cb) => { + cb.setIcon("up-chevron-glyph").setTooltip("Move up").onClick(() => { + arraymove(this.plugin.settings.folder_templates, index, index - 1); + this.plugin.save_settings(); + this.display(); + }); + }).addExtraButton((cb) => { + cb.setIcon("down-chevron-glyph").setTooltip("Move down").onClick(() => { + arraymove(this.plugin.settings.folder_templates, index, index + 1); + this.plugin.save_settings(); + this.display(); + }); + }).addExtraButton((cb) => { + cb.setIcon("cross").setTooltip("Delete").onClick(() => { + this.plugin.settings.folder_templates.splice(index, 1); + this.plugin.save_settings(); + this.display(); + }); + }); + s.infoEl.remove(); + }); + } + add_startup_templates_setting() { + this.containerEl.createEl("h2", { text: "Startup Templates" }); + const desc = document.createDocumentFragment(); + desc.append("Startup Templates are templates that will get executed once when Templater starts.", desc.createEl("br"), "These templates won't output anything.", desc.createEl("br"), "This can be useful to set up templates adding hooks to obsidian events for example."); + new import_obsidian6.Setting(this.containerEl).setDesc(desc); + this.plugin.settings.startup_templates.forEach((template, index) => { + const s = new import_obsidian6.Setting(this.containerEl).addSearch((cb) => { + new FileSuggest(cb.inputEl, this.plugin, FileSuggestMode.TemplateFiles); + cb.setPlaceholder("Example: folder1/template_file").setValue(template).onChange((new_template) => { + if (new_template && this.plugin.settings.startup_templates.contains(new_template)) { + log_error(new TemplaterError("This startup template already exist")); + return; + } + this.plugin.settings.startup_templates[index] = new_template; + this.plugin.save_settings(); + }); + cb.containerEl.addClass("templater_search"); + }).addExtraButton((cb) => { + cb.setIcon("cross").setTooltip("Delete").onClick(() => { + this.plugin.settings.startup_templates.splice(index, 1); + this.plugin.save_settings(); + this.display(); + }); + }); + s.infoEl.remove(); + }); + new import_obsidian6.Setting(this.containerEl).addButton((cb) => { + cb.setButtonText("Add new startup template").setCta().onClick(() => { + this.plugin.settings.startup_templates.push(""); + this.plugin.save_settings(); + this.display(); + }); + }); + } + add_user_script_functions_setting() { + this.containerEl.createEl("h2", { text: "User Script Functions" }); + let desc = document.createDocumentFragment(); + desc.append("All JavaScript files in this folder will be loaded as CommonJS modules, to import custom user functions.", desc.createEl("br"), "The folder needs to be accessible from the vault.", desc.createEl("br"), "Check the ", desc.createEl("a", { + href: "https://silentvoid13.github.io/Templater/", + text: "documentation" + }), " for more information."); + new import_obsidian6.Setting(this.containerEl).setName("Script files folder location").setDesc(desc).addSearch((cb) => { + new FolderSuggest(cb.inputEl); + cb.setPlaceholder("Example: folder1/folder2").setValue(this.plugin.settings.user_scripts_folder).onChange((new_folder) => { + this.plugin.settings.user_scripts_folder = new_folder; + this.plugin.save_settings(); + }); + cb.containerEl.addClass("templater_search"); + }); + desc = document.createDocumentFragment(); + let name; + if (!this.plugin.settings.user_scripts_folder) { + name = "No User Scripts folder set"; + } else { + const files = errorWrapperSync(() => get_tfiles_from_folder(this.plugin.settings.user_scripts_folder), `User Scripts folder doesn't exist`); + if (!files || files.length === 0) { + name = "No User Scripts detected"; + } else { + let count = 0; + for (const file of files) { + if (file.extension === "js") { + count++; + desc.append(desc.createEl("li", { + text: `tp.user.${file.basename}` + })); + } + } + name = `Detected ${count} User Script(s)`; + } + } + new import_obsidian6.Setting(this.containerEl).setName(name).setDesc(desc).addExtraButton((extra) => { + extra.setIcon("sync").setTooltip("Refresh").onClick(() => { + this.display(); + }); + }); + } + add_user_system_command_functions_setting() { + let desc = document.createDocumentFragment(); + desc.append("Allows you to create user functions linked to system commands.", desc.createEl("br"), desc.createEl("b", { + text: "Warning: " + }), "It can be dangerous to execute arbitrary system commands from untrusted sources. Only run system commands that you understand, from trusted sources."); + this.containerEl.createEl("h2", { + text: "User System Command Functions" + }); + new import_obsidian6.Setting(this.containerEl).setName("Enable User System Command Functions").setDesc(desc).addToggle((toggle) => { + toggle.setValue(this.plugin.settings.enable_system_commands).onChange((enable_system_commands) => { + this.plugin.settings.enable_system_commands = enable_system_commands; + this.plugin.save_settings(); + this.display(); + }); + }); + if (this.plugin.settings.enable_system_commands) { + new import_obsidian6.Setting(this.containerEl).setName("Timeout").setDesc("Maximum timeout in seconds for a system command.").addText((text) => { + text.setPlaceholder("Timeout").setValue(this.plugin.settings.command_timeout.toString()).onChange((new_value) => { + const new_timeout = Number(new_value); + if (isNaN(new_timeout)) { + log_error(new TemplaterError("Timeout must be a number")); + return; + } + this.plugin.settings.command_timeout = new_timeout; + this.plugin.save_settings(); + }); + }); + desc = document.createDocumentFragment(); + desc.append("Full path to the shell binary to execute the command with.", desc.createEl("br"), "This setting is optional and will default to the system's default shell if not specified.", desc.createEl("br"), "You can use forward slashes ('/') as path separators on all platforms if in doubt."); + new import_obsidian6.Setting(this.containerEl).setName("Shell binary location").setDesc(desc).addText((text) => { + text.setPlaceholder("Example: /bin/bash, ...").setValue(this.plugin.settings.shell_path).onChange((shell_path) => { + this.plugin.settings.shell_path = shell_path; + this.plugin.save_settings(); + }); + }); + let i = 1; + this.plugin.settings.templates_pairs.forEach((template_pair) => { + const div2 = this.containerEl.createEl("div"); + div2.addClass("templater_div"); + const title = this.containerEl.createEl("h4", { + text: "User Function n\xB0" + i + }); + title.addClass("templater_title"); + const setting2 = new import_obsidian6.Setting(this.containerEl).addExtraButton((extra) => { + extra.setIcon("cross").setTooltip("Delete").onClick(() => { + const index = this.plugin.settings.templates_pairs.indexOf(template_pair); + if (index > -1) { + this.plugin.settings.templates_pairs.splice(index, 1); + this.plugin.save_settings(); + this.display(); + } + }); + }).addText((text) => { + const t = text.setPlaceholder("Function name").setValue(template_pair[0]).onChange((new_value) => { + const index = this.plugin.settings.templates_pairs.indexOf(template_pair); + if (index > -1) { + this.plugin.settings.templates_pairs[index][0] = new_value; + this.plugin.save_settings(); + } + }); + t.inputEl.addClass("templater_template"); + return t; + }).addTextArea((text) => { + const t = text.setPlaceholder("System Command").setValue(template_pair[1]).onChange((new_cmd) => { + const index = this.plugin.settings.templates_pairs.indexOf(template_pair); + if (index > -1) { + this.plugin.settings.templates_pairs[index][1] = new_cmd; + this.plugin.save_settings(); + } + }); + t.inputEl.setAttr("rows", 2); + t.inputEl.addClass("templater_cmd"); + return t; + }); + setting2.infoEl.remove(); + div2.appendChild(title); + div2.appendChild(this.containerEl.lastChild); + i += 1; + }); + const div = this.containerEl.createEl("div"); + div.addClass("templater_div2"); + const setting = new import_obsidian6.Setting(this.containerEl).addButton((button) => { + button.setButtonText("Add New User Function").setCta().onClick(() => { + this.plugin.settings.templates_pairs.push(["", ""]); + this.plugin.save_settings(); + this.display(); + }); + }); + setting.infoEl.remove(); + div.appendChild(this.containerEl.lastChild); + } + } + add_donating_setting() { + const s = new import_obsidian6.Setting(this.containerEl).setName("Donate").setDesc("If you like this Plugin, consider donating to support continued development."); + const a1 = document.createElement("a"); + a1.setAttribute("href", "https://github.com/sponsors/silentvoid13"); + a1.addClass("templater_donating"); + const img1 = document.createElement("img"); + img1.src = "https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86"; + a1.appendChild(img1); + const a2 = document.createElement("a"); + a2.setAttribute("href", "https://www.paypal.com/donate?hosted_button_id=U2SRGAFYXT32Q"); + a2.addClass("templater_donating"); + const img2 = document.createElement("img"); + img2.src = "https://img.shields.io/badge/paypal-silentvoid13-yellow?style=social&logo=paypal"; + a2.appendChild(img2); + s.settingEl.appendChild(a1); + s.settingEl.appendChild(a2); + } +}; + +// src/handlers/FuzzySuggester.ts +var import_obsidian7 = __toModule(require("obsidian")); +var OpenMode; +(function(OpenMode2) { + OpenMode2[OpenMode2["InsertTemplate"] = 0] = "InsertTemplate"; + OpenMode2[OpenMode2["CreateNoteTemplate"] = 1] = "CreateNoteTemplate"; +})(OpenMode || (OpenMode = {})); +var FuzzySuggester = class extends import_obsidian7.FuzzySuggestModal { + constructor(plugin) { + super(app); + this.plugin = plugin; + this.setPlaceholder("Type name of a template..."); + } + getItems() { + if (!this.plugin.settings.templates_folder) { + return app.vault.getMarkdownFiles(); + } + const files = errorWrapperSync(() => get_tfiles_from_folder(this.plugin.settings.templates_folder), `Couldn't retrieve template files from templates folder ${this.plugin.settings.templates_folder}`); + if (!files) { + return []; + } + return files; + } + getItemText(item) { + return item.basename; + } + onChooseItem(item) { + switch (this.open_mode) { + case 0: + this.plugin.templater.append_template_to_active_file(item); + break; + case 1: + this.plugin.templater.create_new_note_from_template(item, this.creation_folder); + break; + } + } + start() { + try { + this.open(); + } catch (e) { + log_error(e); + } + } + insert_template() { + this.open_mode = 0; + this.start(); + } + create_new_note_from_template(folder) { + this.creation_folder = folder; + this.open_mode = 1; + this.start(); + } +}; + +// src/utils/Constants.ts +var UNSUPPORTED_MOBILE_TEMPLATE = "Error_MobileUnsupportedTemplate"; +var ICON_DATA = ``; + +// src/core/Templater.ts +var import_obsidian13 = __toModule(require("obsidian")); + +// src/core/functions/internal_functions/InternalModule.ts +var InternalModule = class { + constructor(plugin) { + this.plugin = plugin; + this.static_functions = new Map(); + this.dynamic_functions = new Map(); + } + getName() { + return this.name; + } + async init() { + await this.create_static_templates(); + this.static_object = Object.fromEntries(this.static_functions); + } + async generate_object(new_config) { + this.config = new_config; + await this.create_dynamic_templates(); + return { + ...this.static_object, + ...Object.fromEntries(this.dynamic_functions) + }; + } +}; + +// src/core/functions/internal_functions/date/InternalModuleDate.ts +var InternalModuleDate = class extends InternalModule { + constructor() { + super(...arguments); + this.name = "date"; + } + async create_static_templates() { + this.static_functions.set("now", this.generate_now()); + this.static_functions.set("tomorrow", this.generate_tomorrow()); + this.static_functions.set("weekday", this.generate_weekday()); + this.static_functions.set("yesterday", this.generate_yesterday()); + } + async create_dynamic_templates() { + } + generate_now() { + return (format2 = "YYYY-MM-DD", offset2, reference2, reference_format) => { + if (reference2 && !window.moment(reference2, reference_format).isValid()) { + throw new TemplaterError("Invalid reference date format, try specifying one with the argument 'reference_format'"); + } + let duration; + if (typeof offset2 === "string") { + duration = window.moment.duration(offset2); + } else if (typeof offset2 === "number") { + duration = window.moment.duration(offset2, "days"); + } + return window.moment(reference2, reference_format).add(duration).format(format2); + }; + } + generate_tomorrow() { + return (format2 = "YYYY-MM-DD") => { + return window.moment().add(1, "days").format(format2); + }; + } + generate_weekday() { + return (format2 = "YYYY-MM-DD", weekday, reference2, reference_format) => { + if (reference2 && !window.moment(reference2, reference_format).isValid()) { + throw new TemplaterError("Invalid reference date format, try specifying one with the argument 'reference_format'"); + } + return window.moment(reference2, reference_format).weekday(weekday).format(format2); + }; + } + generate_yesterday() { + return (format2 = "YYYY-MM-DD") => { + return window.moment().add(-1, "days").format(format2); + }; + } +}; + +// src/core/functions/internal_functions/file/InternalModuleFile.ts +var import_obsidian8 = __toModule(require("obsidian")); +var DEPTH_LIMIT = 10; +var InternalModuleFile = class extends InternalModule { + constructor() { + super(...arguments); + this.name = "file"; + this.include_depth = 0; + this.create_new_depth = 0; + this.linkpath_regex = new RegExp("^\\[\\[(.*)\\]\\]$"); + } + async create_static_templates() { + this.static_functions.set("creation_date", this.generate_creation_date()); + this.static_functions.set("create_new", this.generate_create_new()); + this.static_functions.set("cursor", this.generate_cursor()); + this.static_functions.set("cursor_append", this.generate_cursor_append()); + this.static_functions.set("exists", this.generate_exists()); + this.static_functions.set("find_tfile", this.generate_find_tfile()); + this.static_functions.set("folder", this.generate_folder()); + this.static_functions.set("include", this.generate_include()); + this.static_functions.set("last_modified_date", this.generate_last_modified_date()); + this.static_functions.set("move", this.generate_move()); + this.static_functions.set("path", this.generate_path()); + this.static_functions.set("rename", this.generate_rename()); + this.static_functions.set("selection", this.generate_selection()); + } + async create_dynamic_templates() { + this.dynamic_functions.set("content", await this.generate_content()); + this.dynamic_functions.set("tags", this.generate_tags()); + this.dynamic_functions.set("title", this.generate_title()); + } + async generate_content() { + return await app.vault.read(this.config.target_file); + } + generate_create_new() { + return async (template, filename, open_new = false, folder) => { + this.create_new_depth += 1; + if (this.create_new_depth > DEPTH_LIMIT) { + this.create_new_depth = 0; + throw new TemplaterError("Reached create_new depth limit (max = 10)"); + } + const new_file = await this.plugin.templater.create_new_note_from_template(template, folder, filename, open_new); + this.create_new_depth -= 1; + return new_file; + }; + } + generate_creation_date() { + return (format2 = "YYYY-MM-DD HH:mm") => { + return window.moment(this.config.target_file.stat.ctime).format(format2); + }; + } + generate_cursor() { + return (order2) => { + return `<% tp.file.cursor(${order2 ?? ""}) %>`; + }; + } + generate_cursor_append() { + return (content) => { + const active_view = app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView); + if (active_view === null) { + log_error(new TemplaterError("No active view, can't append to cursor.")); + return; + } + const editor = active_view.editor; + const doc = editor.getDoc(); + doc.replaceSelection(content); + return ""; + }; + } + generate_exists() { + return async (filename) => { + const path = (0, import_obsidian8.normalizePath)(filename); + return await app.vault.exists(path); + }; + } + generate_find_tfile() { + return (filename) => { + const path = (0, import_obsidian8.normalizePath)(filename); + return app.metadataCache.getFirstLinkpathDest(path, ""); + }; + } + generate_folder() { + return (relative = false) => { + const parent = this.config.target_file.parent; + let folder; + if (relative) { + folder = parent.path; + } else { + folder = parent.name; + } + return folder; + }; + } + generate_include() { + return async (include_link) => { + this.include_depth += 1; + if (this.include_depth > DEPTH_LIMIT) { + this.include_depth -= 1; + throw new TemplaterError("Reached inclusion depth limit (max = 10)"); + } + let inc_file_content; + if (include_link instanceof import_obsidian8.TFile) { + inc_file_content = await app.vault.read(include_link); + } else { + let match; + if ((match = this.linkpath_regex.exec(include_link)) === null) { + this.include_depth -= 1; + throw new TemplaterError("Invalid file format, provide an obsidian link between quotes."); + } + const { path, subpath } = (0, import_obsidian8.parseLinktext)(match[1]); + const inc_file = app.metadataCache.getFirstLinkpathDest(path, ""); + if (!inc_file) { + this.include_depth -= 1; + throw new TemplaterError(`File ${include_link} doesn't exist`); + } + inc_file_content = await app.vault.read(inc_file); + if (subpath) { + const cache = app.metadataCache.getFileCache(inc_file); + if (cache) { + const result = (0, import_obsidian8.resolveSubpath)(cache, subpath); + if (result) { + inc_file_content = inc_file_content.slice(result.start.offset, result.end?.offset); + } + } + } + } + try { + const parsed_content = await this.plugin.templater.parser.parse_commands(inc_file_content, this.plugin.templater.current_functions_object); + this.include_depth -= 1; + return parsed_content; + } catch (e) { + this.include_depth -= 1; + throw e; + } + }; + } + generate_last_modified_date() { + return (format2 = "YYYY-MM-DD HH:mm") => { + return window.moment(this.config.target_file.stat.mtime).format(format2); + }; + } + generate_move() { + return async (path, file_to_move) => { + const file = file_to_move || this.config.target_file; + const new_path = (0, import_obsidian8.normalizePath)(`${path}.${file.extension}`); + const dirs = new_path.replace(/\\/g, "/").split("/"); + dirs.pop(); + if (dirs.length) { + const dir = dirs.join("/"); + if (!window.app.vault.getAbstractFileByPath(dir)) { + await window.app.vault.createFolder(dir); + } + } + await app.fileManager.renameFile(file, new_path); + return ""; + }; + } + generate_path() { + return (relative = false) => { + let vault_path = ""; + if (import_obsidian8.Platform.isMobileApp) { + const vault_adapter = app.vault.adapter.fs.uri; + const vault_base = app.vault.adapter.basePath; + vault_path = `${vault_adapter}/${vault_base}`; + } else { + if (app.vault.adapter instanceof import_obsidian8.FileSystemAdapter) { + vault_path = app.vault.adapter.getBasePath(); + } else { + throw new TemplaterError("app.vault is not a FileSystemAdapter instance"); + } + } + if (relative) { + return this.config.target_file.path; + } else { + return `${vault_path}/${this.config.target_file.path}`; + } + }; + } + generate_rename() { + return async (new_title) => { + if (new_title.match(/[\\/:]+/g)) { + throw new TemplaterError("File name cannot contain any of these characters: \\ / :"); + } + const new_path = (0, import_obsidian8.normalizePath)(`${this.config.target_file.parent.path}/${new_title}.${this.config.target_file.extension}`); + await app.fileManager.renameFile(this.config.target_file, new_path); + return ""; + }; + } + generate_selection() { + return () => { + const active_view = app.workspace.getActiveViewOfType(import_obsidian8.MarkdownView); + if (active_view == null) { + throw new TemplaterError("Active view is null, can't read selection."); + } + const editor = active_view.editor; + return editor.getSelection(); + }; + } + generate_tags() { + const cache = app.metadataCache.getFileCache(this.config.target_file); + if (cache) { + return (0, import_obsidian8.getAllTags)(cache); + } + return null; + } + generate_title() { + return this.config.target_file.basename; + } +}; + +// src/core/functions/internal_functions/web/InternalModuleWeb.ts +var InternalModuleWeb = class extends InternalModule { + constructor() { + super(...arguments); + this.name = "web"; + } + async create_static_templates() { + this.static_functions.set("daily_quote", this.generate_daily_quote()); + this.static_functions.set("random_picture", this.generate_random_picture()); + } + async create_dynamic_templates() { + } + async getRequest(url) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new TemplaterError("Error performing GET request"); + } + return response; + } catch (error) { + throw new TemplaterError("Error performing GET request"); + } + } + generate_daily_quote() { + return async () => { + try { + const response = await this.getRequest("https://api.quotable.io/random"); + const json = await response.json(); + const author = json.author; + const quote = json.content; + const new_content = `> ${quote} +> \u2014 ${author}`; + return new_content; + } catch (error) { + new TemplaterError("Error generating daily quote"); + return "Error generating daily quote"; + } + }; + } + generate_random_picture() { + return async (size, query, include_size = false) => { + try { + const response = await this.getRequest(`https://templater-unsplash.fly.dev/${query ? "?q=" + query : ""}`).then((res) => res.json()); + let url = response.full; + if (size && !include_size) { + if (size.includes("x")) { + const [width, height] = size.split("x"); + url = url.concat(`&w=${width}&h=${height}`); + } else { + url = url.concat(`&w=${size}`); + } + } + if (include_size) { + return `![photo by ${response.photog} on Unsplash|${size}](${url})`; + } + return `![photo by ${response.photog} on Unsplash](${url})`; + } catch (error) { + new TemplaterError("Error generating random picture"); + return "Error generating random picture"; + } + }; + } +}; + +// src/core/functions/internal_functions/frontmatter/InternalModuleFrontmatter.ts +var InternalModuleFrontmatter = class extends InternalModule { + constructor() { + super(...arguments); + this.name = "frontmatter"; + } + async create_static_templates() { + } + async create_dynamic_templates() { + const cache = app.metadataCache.getFileCache(this.config.target_file); + this.dynamic_functions = new Map(Object.entries(cache?.frontmatter || {})); + } +}; + +// src/core/functions/internal_functions/system/InternalModuleSystem.ts +var import_obsidian11 = __toModule(require("obsidian")); + +// src/core/functions/internal_functions/system/PromptModal.ts +var import_obsidian9 = __toModule(require("obsidian")); +var PromptModal = class extends import_obsidian9.Modal { + constructor(prompt_text, default_value, multi_line) { + super(app); + this.prompt_text = prompt_text; + this.default_value = default_value; + this.multi_line = multi_line; + this.submitted = false; + } + onOpen() { + this.titleEl.setText(this.prompt_text); + this.createForm(); + } + onClose() { + this.contentEl.empty(); + if (!this.submitted) { + this.reject(); + } + } + createForm() { + const div = this.contentEl.createDiv(); + div.addClass("templater-prompt-div"); + let textInput; + if (this.multi_line) { + textInput = new import_obsidian9.TextAreaComponent(div); + const buttonDiv = this.contentEl.createDiv(); + buttonDiv.addClass("templater-button-div"); + const submitButton = new import_obsidian9.ButtonComponent(buttonDiv); + submitButton.buttonEl.addClass("mod-cta"); + submitButton.setButtonText("Submit").onClick((evt) => { + this.resolveAndClose(evt); + }); + } else { + textInput = new import_obsidian9.TextComponent(div); + } + this.value = this.default_value ?? ""; + textInput.inputEl.addClass("templater-prompt-input"); + textInput.setPlaceholder("Type text here"); + textInput.setValue(this.value); + textInput.onChange((value) => this.value = value); + textInput.inputEl.addEventListener("keydown", (evt) => this.enterCallback(evt)); + } + enterCallback(evt) { + if (this.multi_line) { + if (import_obsidian9.Platform.isDesktop) { + if (evt.shiftKey && evt.key === "Enter") { + } else if (evt.key === "Enter") { + this.resolveAndClose(evt); + } + } else { + if (evt.key === "Enter") { + evt.preventDefault(); + } + } + } else { + if (evt.key === "Enter") { + this.resolveAndClose(evt); + } + } + } + resolveAndClose(evt) { + this.submitted = true; + evt.preventDefault(); + this.resolve(this.value); + this.close(); + } + async openAndGetValue(resolve, reject) { + this.resolve = resolve; + this.reject = reject; + this.open(); + } +}; + +// src/core/functions/internal_functions/system/SuggesterModal.ts +var import_obsidian10 = __toModule(require("obsidian")); +var SuggesterModal = class extends import_obsidian10.FuzzySuggestModal { + constructor(text_items, items, placeholder, limit) { + super(app); + this.text_items = text_items; + this.items = items; + this.submitted = false; + this.setPlaceholder(placeholder); + limit && (this.limit = limit); + } + getItems() { + return this.items; + } + onClose() { + if (!this.submitted) { + this.reject(new TemplaterError("Cancelled prompt")); + } + } + selectSuggestion(value, evt) { + this.submitted = true; + this.close(); + this.onChooseSuggestion(value, evt); + } + getItemText(item) { + if (this.text_items instanceof Function) { + return this.text_items(item); + } + return this.text_items[this.items.indexOf(item)] || "Undefined Text Item"; + } + onChooseItem(item) { + this.resolve(item); + } + async openAndGetValue(resolve, reject) { + this.resolve = resolve; + this.reject = reject; + this.open(); + } +}; + +// src/core/functions/internal_functions/system/InternalModuleSystem.ts +var InternalModuleSystem = class extends InternalModule { + constructor() { + super(...arguments); + this.name = "system"; + } + async create_static_templates() { + this.static_functions.set("clipboard", this.generate_clipboard()); + this.static_functions.set("prompt", this.generate_prompt()); + this.static_functions.set("suggester", this.generate_suggester()); + } + async create_dynamic_templates() { + } + generate_clipboard() { + return async () => { + if (import_obsidian11.Platform.isMobileApp) { + return UNSUPPORTED_MOBILE_TEMPLATE; + } + return await navigator.clipboard.readText(); + }; + } + generate_prompt() { + return async (prompt_text, default_value, throw_on_cancel = false, multi_line = false) => { + const prompt = new PromptModal(prompt_text, default_value, multi_line); + const promise = new Promise((resolve, reject) => prompt.openAndGetValue(resolve, reject)); + try { + return await promise; + } catch (error) { + if (throw_on_cancel) { + throw error; + } + return null; + } + }; + } + generate_suggester() { + return async (text_items, items, throw_on_cancel = false, placeholder = "", limit) => { + const suggester = new SuggesterModal(text_items, items, placeholder, limit); + const promise = new Promise((resolve, reject) => suggester.openAndGetValue(resolve, reject)); + try { + return await promise; + } catch (error) { + if (throw_on_cancel) { + throw error; + } + return null; + } + }; + } +}; + +// src/core/functions/internal_functions/config/InternalModuleConfig.ts +var InternalModuleConfig = class extends InternalModule { + constructor() { + super(...arguments); + this.name = "config"; + } + async create_static_templates() { + } + async create_dynamic_templates() { + } + async generate_object(config) { + return config; + } +}; + +// src/core/functions/internal_functions/InternalFunctions.ts +var InternalFunctions = class { + constructor(plugin) { + this.plugin = plugin; + this.modules_array = []; + this.modules_array.push(new InternalModuleDate(this.plugin)); + this.modules_array.push(new InternalModuleFile(this.plugin)); + this.modules_array.push(new InternalModuleWeb(this.plugin)); + this.modules_array.push(new InternalModuleFrontmatter(this.plugin)); + this.modules_array.push(new InternalModuleSystem(this.plugin)); + this.modules_array.push(new InternalModuleConfig(this.plugin)); + } + async init() { + for (const mod of this.modules_array) { + await mod.init(); + } + } + async generate_object(config) { + const internal_functions_object = {}; + for (const mod of this.modules_array) { + internal_functions_object[mod.getName()] = await mod.generate_object(config); + } + return internal_functions_object; + } +}; + +// src/core/functions/user_functions/UserSystemFunctions.ts +var import_child_process = __toModule(require("child_process")); +var import_util = __toModule(require("util")); +var import_obsidian12 = __toModule(require("obsidian")); +var UserSystemFunctions = class { + constructor(plugin) { + this.plugin = plugin; + if (import_obsidian12.Platform.isMobileApp || !(app.vault.adapter instanceof import_obsidian12.FileSystemAdapter)) { + this.cwd = ""; + } else { + this.cwd = app.vault.adapter.getBasePath(); + this.exec_promise = (0, import_util.promisify)(import_child_process.exec); + } + } + async generate_system_functions(config) { + const user_system_functions = new Map(); + const internal_functions_object = await this.plugin.templater.functions_generator.generate_object(config, FunctionsMode.INTERNAL); + for (const template_pair of this.plugin.settings.templates_pairs) { + const template = template_pair[0]; + let cmd = template_pair[1]; + if (!template || !cmd) { + continue; + } + if (import_obsidian12.Platform.isMobileApp) { + user_system_functions.set(template, () => { + return new Promise((resolve) => resolve(UNSUPPORTED_MOBILE_TEMPLATE)); + }); + } else { + cmd = await this.plugin.templater.parser.parse_commands(cmd, internal_functions_object); + user_system_functions.set(template, async (user_args) => { + const process_env = { + ...process.env, + ...user_args + }; + const cmd_options = { + timeout: this.plugin.settings.command_timeout * 1e3, + cwd: this.cwd, + env: process_env, + ...this.plugin.settings.shell_path && { + shell: this.plugin.settings.shell_path + } + }; + try { + const { stdout } = await this.exec_promise(cmd, cmd_options); + return stdout.trimRight(); + } catch (error) { + throw new TemplaterError(`Error with User Template ${template}`, error); + } + }); + } + } + return user_system_functions; + } + async generate_object(config) { + const user_system_functions = await this.generate_system_functions(config); + return Object.fromEntries(user_system_functions); + } +}; + +// src/core/functions/user_functions/UserScriptFunctions.ts +var UserScriptFunctions = class { + constructor(plugin) { + this.plugin = plugin; + } + async generate_user_script_functions() { + const user_script_functions = new Map(); + const files = errorWrapperSync(() => get_tfiles_from_folder(this.plugin.settings.user_scripts_folder), `Couldn't find user script folder "${this.plugin.settings.user_scripts_folder}"`); + if (!files) { + return new Map(); + } + for (const file of files) { + if (file.extension.toLowerCase() === "js") { + await this.load_user_script_function(file, user_script_functions); + } + } + return user_script_functions; + } + async load_user_script_function(file, user_script_functions) { + const req = (s) => { + return window.require && window.require(s); + }; + const exp = {}; + const mod = { + exports: exp + }; + const file_content = await app.vault.read(file); + const wrapping_fn = window.eval("(function anonymous(require, module, exports){" + file_content + "\n})"); + wrapping_fn(req, mod, exp); + const user_function = exp["default"] || mod.exports; + if (!user_function) { + throw new TemplaterError(`Failed to load user script ${file.path}. No exports detected.`); + } + if (!(user_function instanceof Function)) { + throw new TemplaterError(`Failed to load user script ${file.path}. Default export is not a function.`); + } + user_script_functions.set(`${file.basename}`, user_function); + } + async generate_object() { + const user_script_functions = await this.generate_user_script_functions(); + return Object.fromEntries(user_script_functions); + } +}; + +// src/core/functions/user_functions/UserFunctions.ts +var UserFunctions = class { + constructor(plugin) { + this.plugin = plugin; + this.user_system_functions = new UserSystemFunctions(plugin); + this.user_script_functions = new UserScriptFunctions(plugin); + } + async generate_object(config) { + let user_system_functions = {}; + let user_script_functions = {}; + if (this.plugin.settings.enable_system_commands) { + user_system_functions = await this.user_system_functions.generate_object(config); + } + if (this.plugin.settings.user_scripts_folder) { + user_script_functions = await this.user_script_functions.generate_object(); + } + return { + ...user_system_functions, + ...user_script_functions + }; + } +}; + +// src/core/functions/FunctionsGenerator.ts +var obsidian_module = __toModule(require("obsidian")); +var FunctionsMode; +(function(FunctionsMode2) { + FunctionsMode2[FunctionsMode2["INTERNAL"] = 0] = "INTERNAL"; + FunctionsMode2[FunctionsMode2["USER_INTERNAL"] = 1] = "USER_INTERNAL"; +})(FunctionsMode || (FunctionsMode = {})); +var FunctionsGenerator = class { + constructor(plugin) { + this.plugin = plugin; + this.internal_functions = new InternalFunctions(this.plugin); + this.user_functions = new UserFunctions(this.plugin); + } + async init() { + await this.internal_functions.init(); + } + additional_functions() { + return { + obsidian: obsidian_module + }; + } + async generate_object(config, functions_mode = 1) { + const final_object = {}; + const additional_functions_object = this.additional_functions(); + const internal_functions_object = await this.internal_functions.generate_object(config); + let user_functions_object = {}; + Object.assign(final_object, additional_functions_object); + switch (functions_mode) { + case 0: + Object.assign(final_object, internal_functions_object); + break; + case 1: + user_functions_object = await this.user_functions.generate_object(config); + Object.assign(final_object, { + ...internal_functions_object, + user: user_functions_object + }); + break; + } + return final_object; + } +}; + +// node_modules/@silentvoid13/rusty_engine/rusty_engine.js +var import_meta = {}; +var wasm; +var heap = new Array(32).fill(void 0); +heap.push(void 0, null, true, false); +function getObject(idx) { + return heap[idx]; +} +var heap_next = heap.length; +function dropObject(idx) { + if (idx < 36) + return; + heap[idx] = heap_next; + heap_next = idx; +} +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} +var cachedTextDecoder = new TextDecoder("utf-8", { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +var cachedUint8Memory0 = new Uint8Array(); +function getUint8Memory0() { + if (cachedUint8Memory0.byteLength === 0) { + cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8Memory0; +} +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} +function addHeapObject(obj) { + if (heap_next === heap.length) + heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + heap[idx] = obj; + return idx; +} +var WASM_VECTOR_LEN = 0; +var cachedTextEncoder = new TextEncoder("utf-8"); +var encodeString = typeof cachedTextEncoder.encodeInto === "function" ? function(arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} : function(arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}; +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === void 0) { + const buf = cachedTextEncoder.encode(arg); + const ptr2 = malloc(buf.length); + getUint8Memory0().subarray(ptr2, ptr2 + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr2; + } + let len = arg.length; + let ptr = malloc(len); + const mem = getUint8Memory0(); + let offset2 = 0; + for (; offset2 < len; offset2++) { + const code = arg.charCodeAt(offset2); + if (code > 127) + break; + mem[ptr + offset2] = code; + } + if (offset2 !== len) { + if (offset2 !== 0) { + arg = arg.slice(offset2); + } + ptr = realloc(ptr, len, len = offset2 + arg.length * 3); + const view = getUint8Memory0().subarray(ptr + offset2, ptr + len); + const ret = encodeString(arg, view); + offset2 += ret.written; + } + WASM_VECTOR_LEN = offset2; + return ptr; +} +function isLikeNone(x) { + return x === void 0 || x === null; +} +var cachedInt32Memory0 = new Int32Array(); +function getInt32Memory0() { + if (cachedInt32Memory0.byteLength === 0) { + cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32Memory0; +} +function debugString(val) { + const type = typeof val; + if (type == "number" || type == "boolean" || val == null) { + return `${val}`; + } + if (type == "string") { + return `"${val}"`; + } + if (type == "symbol") { + const description = val.description; + if (description == null) { + return "Symbol"; + } else { + return `Symbol(${description})`; + } + } + if (type == "function") { + const name = val.name; + if (typeof name == "string" && name.length > 0) { + return `Function(${name})`; + } else { + return "Function"; + } + } + if (Array.isArray(val)) { + const length = val.length; + let debug = "["; + if (length > 0) { + debug += debugString(val[0]); + } + for (let i = 1; i < length; i++) { + debug += ", " + debugString(val[i]); + } + debug += "]"; + return debug; + } + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + return toString.call(val); + } + if (className == "Object") { + try { + return "Object(" + JSON.stringify(val) + ")"; + } catch (_) { + return "Object"; + } + } + if (val instanceof Error) { + return `${val.name}: ${val.message} +${val.stack}`; + } + return className; +} +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } + return instance.ptr; +} +var stack_pointer = 32; +function addBorrowedObject(obj) { + if (stack_pointer == 1) + throw new Error("out of js stack"); + heap[--stack_pointer] = obj; + return stack_pointer; +} +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_exn_store(addHeapObject(e)); + } +} +var ParserConfig = class { + static __wrap(ptr) { + const obj = Object.create(ParserConfig.prototype); + obj.ptr = ptr; + return obj; + } + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_parserconfig_free(ptr); + } + get interpolate() { + const ret = wasm.__wbg_get_parserconfig_interpolate(this.ptr); + return String.fromCodePoint(ret); + } + set interpolate(arg0) { + wasm.__wbg_set_parserconfig_interpolate(this.ptr, arg0.codePointAt(0)); + } + get execution() { + const ret = wasm.__wbg_get_parserconfig_execution(this.ptr); + return String.fromCodePoint(ret); + } + set execution(arg0) { + wasm.__wbg_set_parserconfig_execution(this.ptr, arg0.codePointAt(0)); + } + get single_whitespace() { + const ret = wasm.__wbg_get_parserconfig_single_whitespace(this.ptr); + return String.fromCodePoint(ret); + } + set single_whitespace(arg0) { + wasm.__wbg_set_parserconfig_single_whitespace(this.ptr, arg0.codePointAt(0)); + } + get multiple_whitespace() { + const ret = wasm.__wbg_get_parserconfig_multiple_whitespace(this.ptr); + return String.fromCodePoint(ret); + } + set multiple_whitespace(arg0) { + wasm.__wbg_set_parserconfig_multiple_whitespace(this.ptr, arg0.codePointAt(0)); + } + constructor(opt, clt, inte, ex, sw, mw, gv) { + const ptr0 = passStringToWasm0(opt, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(clt, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passStringToWasm0(gv, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len2 = WASM_VECTOR_LEN; + const ret = wasm.parserconfig_new(ptr0, len0, ptr1, len1, inte.codePointAt(0), ex.codePointAt(0), sw.codePointAt(0), mw.codePointAt(0), ptr2, len2); + return ParserConfig.__wrap(ret); + } + get opening_tag() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.parserconfig_opening_tag(retptr, this.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } + set opening_tag(val) { + const ptr0 = passStringToWasm0(val, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.parserconfig_set_opening_tag(this.ptr, ptr0, len0); + } + get closing_tag() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.parserconfig_closing_tag(retptr, this.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } + set closing_tag(val) { + const ptr0 = passStringToWasm0(val, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.parserconfig_set_closing_tag(this.ptr, ptr0, len0); + } + get global_var() { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.parserconfig_global_var(retptr, this.ptr); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_free(r0, r1); + } + } + set global_var(val) { + const ptr0 = passStringToWasm0(val, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.parserconfig_set_global_var(this.ptr, ptr0, len0); + } +}; +var Renderer = class { + static __wrap(ptr) { + const obj = Object.create(Renderer.prototype); + obj.ptr = ptr; + return obj; + } + __destroy_into_raw() { + const ptr = this.ptr; + this.ptr = 0; + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_renderer_free(ptr); + } + constructor(config) { + _assertClass(config, ParserConfig); + var ptr0 = config.ptr; + config.ptr = 0; + const ret = wasm.renderer_new(ptr0); + return Renderer.__wrap(ret); + } + render_content(content, context) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(content, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + wasm.renderer_render_content(retptr, this.ptr, ptr0, len0, addBorrowedObject(context)); + var r0 = getInt32Memory0()[retptr / 4 + 0]; + var r1 = getInt32Memory0()[retptr / 4 + 1]; + var r2 = getInt32Memory0()[retptr / 4 + 2]; + if (r2) { + throw takeObject(r1); + } + return takeObject(r0); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + heap[stack_pointer++] = void 0; + } + } +}; +async function load(module2, imports) { + if (typeof Response === "function" && module2 instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === "function") { + try { + return await WebAssembly.instantiateStreaming(module2, imports); + } catch (e) { + if (module2.headers.get("Content-Type") != "application/wasm") { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + } else { + throw e; + } + } + } + const bytes = await module2.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module2, imports); + if (instance instanceof WebAssembly.Instance) { + return { instance, module: module2 }; + } else { + return instance; + } + } +} +function getImports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_object_drop_ref = function(arg0) { + takeObject(arg0); + }; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }; + imports.wbg.__wbindgen_string_get = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof obj === "string" ? obj : void 0; + var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbg_call_97ae9d8645dc388b = function() { + return handleError(function(arg0, arg1) { + const ret = getObject(arg0).call(getObject(arg1)); + return addHeapObject(ret); + }, arguments); + }; + imports.wbg.__wbg_new_8d2af00bc1e329ee = function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_message_fe2af63ccc8985bc = function(arg0) { + const ret = getObject(arg0).message; + return addHeapObject(ret); + }; + imports.wbg.__wbg_newwithargs_8fe23e3842840c8e = function(arg0, arg1, arg2, arg3) { + const ret = new Function(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3)); + return addHeapObject(ret); + }; + imports.wbg.__wbg_call_168da88779e35f61 = function() { + return handleError(function(arg0, arg1, arg2) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments); + }; + imports.wbg.__wbg_call_3999bee59e9f7719 = function() { + return handleError(function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2), getObject(arg3)); + return addHeapObject(ret); + }, arguments); + }; + imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + return imports; +} +function initMemory(imports, maybe_memory) { +} +function finalizeInit(instance, module2) { + wasm = instance.exports; + init.__wbindgen_wasm_module = module2; + cachedInt32Memory0 = new Int32Array(); + cachedUint8Memory0 = new Uint8Array(); + return wasm; +} +async function init(input) { + if (typeof input === "undefined") { + input = new URL("rusty_engine_bg.wasm", import_meta.url); + } + const imports = getImports(); + if (typeof input === "string" || typeof Request === "function" && input instanceof Request || typeof URL === "function" && input instanceof URL) { + input = fetch(input); + } + initMemory(imports); + const { instance, module: module2 } = await load(await input, imports); + return finalizeInit(instance, module2); +} +var rusty_engine_default = init; + +// wasm-embed:/home/runner/work/Templater/Templater/node_modules/@silentvoid13/rusty_engine/rusty_engine_bg.wasm +var rusty_engine_bg_default = __toBinary("AGFzbQEAAAABvwEaYAJ/fwBgAn9/AX9gAX8Bf2ADf39/AX9gA39/fwBgAX8AYAV/f39/fwBgBH9/f38AYAR/f39/AX9gAABgBX9/f39/AX9gAX8BfmAAAX9gBn9/f39/fwBgB39/f39/f38AYAV/f35/fwBgBX9/fX9/AGAFf398f38AYAR/fn9/AGAFf35/f38AYAR/fX9/AGAEf3x/fwBgBn9/f39/fwF/YAd/f39/f39/AX9gCn9/f39/f39/f38Bf2ACfn8BfwLkAgsDd2JnGl9fd2JpbmRnZW5fb2JqZWN0X2Ryb3BfcmVmAAUDd2JnFV9fd2JpbmRnZW5fc3RyaW5nX25ldwABA3diZxVfX3diaW5kZ2VuX3N0cmluZ19nZXQAAAN3YmcbX193YmdfY2FsbF85N2FlOWQ4NjQ1ZGMzODhiAAEDd2JnGl9fd2JnX25ld184ZDJhZjAwYmMxZTMyOWVlAAEDd2JnHl9fd2JnX21lc3NhZ2VfZmUyYWY2M2NjYzg5ODViYwACA3diZyJfX3diZ19uZXd3aXRoYXJnc184ZmUyM2UzODQyODQwYzhlAAgDd2JnG19fd2JnX2NhbGxfMTY4ZGE4ODc3OWUzNWY2MQADA3diZxtfX3diZ19jYWxsXzM5OTliZWU1OWU5Zjc3MTkACAN3YmcXX193YmluZGdlbl9kZWJ1Z19zdHJpbmcAAAN3YmcQX193YmluZGdlbl90aHJvdwAAA7kBtwECBwAGAgYEBAcBBQMKCAAEBgYAAwcCAAEADgETAQQXAQICAQAAAwcZAQAFAQwABgACAgAAAgAEBAAGAQAAAAAEBw0CAQUEBQYCDBgAAQAAAAQBAQEAAQABBAQEBgMDBwMJAwQIAAAABQkAAgEAAAAABwAAAgICAgAFBQMEFgoGEQ8QAAUHAwIBAgABBQEBCAACAQEBBQEAAgECAgACAQEBAgAJCQICAgIAAAAAAwMDAQECAgsLCwUEBQFwATs7BQMBABEGCQF/AUGAgMAACwfcBRkGbWVtb3J5AgAXX193YmdfcGFyc2VyY29uZmlnX2ZyZWUAUSJfX3diZ19nZXRfcGFyc2VyY29uZmlnX2ludGVycG9sYXRlAH4iX193Ymdfc2V0X3BhcnNlcmNvbmZpZ19pbnRlcnBvbGF0ZQB3IF9fd2JnX2dldF9wYXJzZXJjb25maWdfZXhlY3V0aW9uAH8gX193Ymdfc2V0X3BhcnNlcmNvbmZpZ19leGVjdXRpb24AeChfX3diZ19nZXRfcGFyc2VyY29uZmlnX3NpbmdsZV93aGl0ZXNwYWNlAIABKF9fd2JnX3NldF9wYXJzZXJjb25maWdfc2luZ2xlX3doaXRlc3BhY2UAeSpfX3diZ19nZXRfcGFyc2VyY29uZmlnX211bHRpcGxlX3doaXRlc3BhY2UAgQEqX193Ymdfc2V0X3BhcnNlcmNvbmZpZ19tdWx0aXBsZV93aGl0ZXNwYWNlAHoQcGFyc2VyY29uZmlnX25ldwBVGHBhcnNlcmNvbmZpZ19vcGVuaW5nX3RhZwBGHHBhcnNlcmNvbmZpZ19zZXRfb3BlbmluZ190YWcAYxhwYXJzZXJjb25maWdfY2xvc2luZ190YWcARxxwYXJzZXJjb25maWdfc2V0X2Nsb3NpbmdfdGFnAGQXcGFyc2VyY29uZmlnX2dsb2JhbF92YXIASBtwYXJzZXJjb25maWdfc2V0X2dsb2JhbF92YXIAZRNfX3diZ19yZW5kZXJlcl9mcmVlAE8McmVuZGVyZXJfbmV3ACAXcmVuZGVyZXJfcmVuZGVyX2NvbnRlbnQAORFfX3diaW5kZ2VuX21hbGxvYwB1El9fd2JpbmRnZW5fcmVhbGxvYwCFAR9fX3diaW5kZ2VuX2FkZF90b19zdGFja19wb2ludGVyAKsBD19fd2JpbmRnZW5fZnJlZQCaARRfX3diaW5kZ2VuX2V4bl9zdG9yZQCfAQllAQBBAQs6mAGdAaoBPzzBAZUBlgFOkgGOAWotYsEBwQFnKl3BAXaIAUyJAYgBhwGQAY8BiQGJAYwBigGLAZgBX8EBaKABXo4BvwG+AYQBOElwoQHBAWioAWCjAVclqQGcAcEBwAEK2dYCtwG8IAIPfwF+IwBBEGsiCyQAAkACQCAAQfUBTwRAQYCAfEEIQQgQlwFBFEEIEJcBakEQQQgQlwFqa0F3cUF9aiICQQBBEEEIEJcBQQJ0ayIBIAEgAksbIABNDQIgAEEEakEIEJcBIQRBrK7AACgCAEUNAUEAIARrIQMCQAJAAn9BACAEQYACSQ0AGkEfIARB////B0sNABogBEEGIARBCHZnIgBrdkEBcSAAQQF0a0E+agsiBkECdEG4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EBBBIAQoAgAiBSAAKAIAaiIGQdSDwAApAAA3AAAgBCAFQRBqNgIAIAZBCGpB3IPAACkAADcAACADQQE2AiwgAyABKAIIQShqIgU2AiggAyAANgIYIANB3ABqQQE2AgAgA0ICNwJMIANB8IPAADYCSCADIANBKGo2AlgCQAJAAkACQAJAAkAgA0EYakGYisAAIANByABqEB5FBEAgAigCACEIAkAgAigCCCIBRQ0AIAFBBHQhCkGQhMAAIQZBACEBQQAhBANAAn8gASAIaiIHQQRqIgkgBygCAEUNABoCQCAERQ0AIANBEGogBCgCACAEKAIEQQAgBiAGLQAAQQJGG0EBEBAgA0EIaiADKAIQIAMoAhRBACAHQQ1qIgQgBC0AAEECRhtBABAQIANBGGogAygCCCADKAIMEBIgA0EBNgI0IANBATYCLCADIAU2AiggAyADQRhqNgIwIAMgADYCRCADQQI2AlwgA0IDNwJMIANBmITAADYCSCADIANBKGo2AlggA0HEAGpBmIrAACADQcgAahAeDQUgAygCHEUNACADKAIYEBULIAdBDmohBgJAIAdBDGotAABFBEAgA0ECNgIsIAMgCTYCKCADIAA2AhggA0EBNgJcIANCAjcCTCADQfSEwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQcgA0ECNgI0IANBoIXAADYCMCADQQE2AiwgAyAFNgIoIAMgADYCGCADQQI2AlwgA0IDNwJMIANBmITAADYCSCADIANBKGo2AlggA0EYakGYisAAIANByABqEB5FDQFBq4HAAEErIANByABqQdiBwABBqIXAABBSAAsgA0ECNgIsIAMgCTYCKCADIAA2AhggA0EBNgJcIANCAjcCTCADQcSEwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQcLQQALIQQgCiABQRBqIgFHDQALIARFDQAgAyAEKAIAIAQoAgRBACAGIAYtAABBAkYbQQEQECADQRhqIAMoAgAgAygCBBASIANBNGpBATYCACADQQE2AiwgAyAFNgIoIAMgA0EYajYCMCADIAA2AkQgA0HcAGpBAjYCACADQgM3AkwgA0GYhMAANgJIIAMgA0EoajYCWCADQcQAakGYisAAIANByABqEB4NBSADKAIcRQ0AIAMoAhgQFQsgAEEEaigCACAAQQhqIgQoAgAiAWtBJ00EQCAAIAFBKBBBIAQoAgAhAQsgBCABQShqNgIAIAAoAgAgAWoiAUHIhcAAKQAANwAAIAFBCGpB0IXAACkAADcAACABQRBqQdiFwAApAAA3AAAgAUEYakHghcAAKQAANwAAIAFBIGpB6IXAACkAADcAACADQTxqQQI2AgAgA0E0akEBNgIAIANBoIXAADYCOCADIAU2AjAgA0EBNgIsIAMgBTYCKCADIAA2AhggA0HcAGoiAUEDNgIAIANCBDcCTCADQZiGwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQUgA0EBNgIsIAMgBTYCKCADIAA2AhggAUEBNgIAIANCAjcCTCADQdCGwAA2AkggAyADQShqNgJYIANBGGpBmIrAACADQcgAahAeDQYgAkEEaigCAARAIAgQFQsgA0HgAGokAA8LQauBwABBKyADQcgAakHYgcAAQYCEwAAQUgALQauBwABBKyADQcgAakHYgcAAQbCEwAAQUgALQauBwABBKyADQcgAakHYgcAAQYSFwAAQUgALQauBwABBKyADQcgAakHYgcAAQdSEwAAQUgALQauBwABBKyADQcgAakHYgcAAQbiFwAAQUgALQauBwABBKyADQcgAakHYgcAAQbiGwAAQUgALQauBwABBKyADQcgAakHYgcAAQeCGwAAQUgAL7QkCCH8GfiMAQdAAayIDJAACQAJAAkAQVCIEBEAgA0EgakIANwMAIANBHGpBkIrAADYCACAEIAQpAwAiC0IBfDcDACADQQA2AhggAyALNwMIIAMgBEEIaikDADcDECADQqeAgIDwBDcDSCADQo2AgICgDjcDQCADQoqAgIDgDTcDOCADQtyAgIDACzcDMCADQQhqIANBMGoQGSADQQA2AjggA0IENwMwIAJFBEAgAEEANgIIIABCATcCAEEEIQRBBCEBDAQLIAEgAmohCEEAIQIDQAJ/IAEsAAAiBEF/SgRAIARB/wFxIQQgAUEBagwBCyABLQABQT9xIQUgBEEfcSEGIARBX00EQCAGQQZ0IAVyIQQgAUECagwBCyABLQACQT9xIAVBBnRyIQUgBEFwSQRAIAUgBkEMdHIhBCABQQNqDAELIAZBEnRBgIDwAHEgAS0AA0E/cSAFQQZ0cnIiBEGAgMQA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ASACIANqQQFqIgM2AhAgAyAHT0EAIAMgDE0bRQRAIAYgA08NAQwDCwsgB0EEELUBAAsgASAGNgIQCyABLQAkIAEoAgAiAiABKAIEIgRHckUNASABQQE6ACUgBCACawshAyAIRQ0AIAIgCGohCyADRQRAQQAhAgwBCyADQX9qIgEgAyABIAtqLQAAQQ1GGyECCyAAIAI2AgQgACALNgIAIAVBEGokAAv+BAEKfyMAQTBrIgMkACADQSRqIAE2AgAgA0EDOgAoIANCgICAgIAENwMIIAMgADYCICADQQA2AhggA0EANgIQAkACQAJAIAIoAggiCkUEQCACQRRqKAIAIgRFDQEgAigCACEBIAIoAhAhACAEQX9qQf////8BcUEBaiIHIQQDQCABQQRqKAIAIgUEQCADKAIgIAEoAgAgBSADKAIkKAIMEQMADQQLIAAoAgAgA0EIaiAAQQRqKAIAEQEADQMgAEEIaiEAIAFBCGohASAEQX9qIgQNAAsMAQsgAkEMaigCACIARQ0AIABBBXQhCyAAQX9qQf///z9xQQFqIQcgAigCACEBA0AgAUEEaigCACIABEAgAygCICABKAIAIAAgAygCJCgCDBEDAA0DCyADIAQgCmoiBUEcai0AADoAKCADIAVBBGopAgBCIIk3AwggBUEYaigCACEGIAIoAhAhCEEAIQlBACEAAkACQAJAIAVBFGooAgBBAWsOAgACAQsgBkEDdCAIaiIMKAIEQTdHDQEgDCgCACgCACEGC0EBIQALIAMgBjYCFCADIAA2AhAgBUEQaigCACEAAkACQAJAIAVBDGooAgBBAWsOAgACAQsgAEEDdCAIaiIGKAIEQTdHDQEgBigCACgCACEAC0EBIQkLIAMgADYCHCADIAk2AhggCCAFKAIAQQN0aiIAKAIAIANBCGogACgCBBEBAA0CIAFBCGohASALIARBIGoiBEcNAAsLQQAhACAHIAIoAgRJIgFFDQEgAygCICACKAIAIAdBA3RqQQAgARsiASgCACABKAIEIAMoAiQoAgwRAwBFDQELQQEhAAsgA0EwaiQAIAALwgQBCH8jAEHQAGsiBCQAIARBEGogASACIAMoAgAgA0EIaigCABAOAkACQAJAAkACQAJAIAQoAhBFBEAgBEEeai0AAA0EIARBxABqKAIAIQYgBCgCQCEHIARBHGot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AkAgAUGAAU8EQCACQQA2AgwgAUGAEEkNASABQYCABE8NAiACIAFBP3FBgAFyOgAOIAIgAUEMdkHgAXI6AAwgAiABQQZ2QT9xQYABcjoADUEDDAMLIAAoAggiAyAAQQRqKAIARgR/IAAgAxBAIAAoAggFIAMLIAAoAgBqIAE6AAAgACAAKAIIQQFqNgIIDAMLIAIgAUE/cUGAAXI6AA0gAiABQQZ2QcABcjoADEECDAELIAIgAUE/cUGAAXI6AA8gAiABQQZ2QT9xQYABcjoADiACIAFBDHZBP3FBgAFyOgANIAIgAUESdkEHcUHwAXI6AAxBBAshASAAQQRqKAIAIABBCGoiBCgCACIDayABSQRAIAAgAyABEEEgBCgCACEDCyAAKAIAIANqIAJBDGogARC5ARogBCABIANqNgIACyACQRBqJABBAAvVAgEDfyMAQRBrIgIkAAJAAn8CQCABQYABTwRAIAJBADYCDCABQYAQTw0BIAIgAUE/cUGAAXI6AA0gAiABQQZ2QcABcjoADEECDAILIAAoAggiAyAAQQRqKAIARgRAIAAgAxBDIAAoAgghAwsgACADQQFqNgIIIAAoAgAgA2ogAToAAAwCCyABQYCABE8EQCACIAFBP3FBgAFyOgAPIAIgAUEGdkE/cUGAAXI6AA4gAiABQQx2QT9xQYABcjoADSACIAFBEnZBB3FB8AFyOgAMQQQMAQsgAiABQT9xQYABcjoADiACIAFBDHZB4AFyOgAMIAIgAUEGdkE/cUGAAXI6AA1BAwshASAAQQRqKAIAIABBCGoiBCgCACIDayABSQRAIAAgAyABEEIgBCgCACEDCyAAKAIAIANqIAJBDGogARC5ARogBCABIANqNgIACyACQRBqJAAL1wIBA38jAEEQayICJAACQAJ/AkACQCABQYABTwRAIAJBADYCDCABQYAQSQ0BIAFBgIAETw0CIAIgAUE/cUGAAXI6AA4gAiABQQx2QeABcjoADCACIAFBBnZBP3FBgAFyOgANQQMMAwsgACgCCCIDIABBBGooAgBGBEAgACADEEMgACgCCCEDCyAAIANBAWo2AgggACgCACADaiABOgAADAMLIAIgAUE/cUGAAXI6AA0gAiABQQZ2QcABcjoADEECDAELIAIgAUE/cUGAAXI6AA8gAiABQQZ2QT9xQYABcjoADiAC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AiADKAIIIgRNBEAgASECDAELIARFBEBBASECIAEQFQwBCyABIAJBASAEEJkBIgJFDQMLIAAgBDYCBCAAIAI2AgAgA0EQaiQADwsQrQEACxCuAQALIARBARCzAQALrAEBA38jAEEQayIDJAACQAJAIAEEQCABKAIAIgJBf0YNASABIAJBAWo2AgAgAyABQRBqEGEgASABKAIAQX9qNgIAIAMoAgAhAQJAIAMoAgQiAiADKAIIIgRNBEAgASECDAELIARFBEBBASECIAEQFQwBCyABIAJBASAEEJkBIgJFDQMLIAAgBDYCBCAAIAI2AgAgA0EQaiQADwsQrQEACxCuAQALIARBARCzAQALrAEBA38jAEEQayIDJAACQAJAIAEEQCABKAIAIgJBf0YNASABIAJBAWo2AgAgAyABQSxqEGEgASABKAIAQX9qNgIAIAMoAgAhAQJAIAMoAgQiAiADKAIIIgRNBEAgASECDAELIARFBEBBASECIAEQFQwBCyABIAJBASAEEJkBIgJFDQMLIAAgBDYCBCAAIAI2AgAgA0EQaiQADwsQrQEACxCuAQALIARBARCzAQALrAEBA38jAEEwayICJAAgAUEEaiEDIAEoAgRFBEAgASgCACEBIAJBEGoiBEEANgIAIAJCATcDCCACIAJBCGo2AhQgAkEoaiABQRBqKQIANwMAIAJBIGogAUEIaikCADcDACACIAEpAgA3AxggAkEUakGAjsAAIAJBGGoQHhogA0EIaiAEKAIANgIAIAMgAikDCDcCAAsgAEHoj8AANgIEIAAgAzYCACACQTBqJAALkAEBAn8CQAJ/AkACQAJAAn9BASIDIAFBAEgNABogAigCCEUNAiACKAIEIgQNASABDQNBAQwECyEDQQAhAQwECyACKAIAIARBASABEJkBDAILIAENAEEBDAELIAFBARCeAQsiAgRAIAAgAjYCBEEAIQMMAQsgACABNgIEQQEhAQsgACADNgIAIABBCGogATYCAAunAQECfwJAAkACQAJAAkACQAJAAn8gAgRAQQEiBCABQQBIDQEaIAMoAghFDQMgAygCBCIFDQIgAQ0EDAYLIAAgATYCBEEBCyEEQQAhAQwGCyADKAIAIAUgAiABEJkBIgNFDQIMBAsgAUUNAgsgASACEJ4BIgMNAgsgACABNgIEIAIhAQwCCyACIQML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AX8jAEEwayICJAAgAiABNgIEIAIgADYCACACQRxqQQI2AgAgAkEsakEDNgIAIAJCAjcCDCACQfSWwAA2AgggAkEDNgIkIAIgAkEgajYCGCACIAJBBGo2AiggAiACNgIgIAJBCGpBhJfAABB0AAtsAQF/IwBBMGsiAyQAIAMgATYCBCADIAA2AgAgA0EcakECNgIAIANBLGpBAzYCACADQgI3AgwgA0HAk8AANgIIIANBAzYCJCADIANBIGo2AhggAyADNgIoIAMgA0EEajYCICADQQhqIAIQdAALVgECfyMAQSBrIgIkACAAQRxqKAIAIQMgACgCGCACQRhqIAFBEGopAgA3AwAgAkEQaiABQQhqKQIANwMAIAIgASkCADcDCCADIAJBCGoQHiACQSBqJAALWQEBfyMAQSBrIgIkACACIAAoAgA2AgQgAkEYaiABQRBqKQIANwMAIAJBEGogAUEIaikCADcDACACIAEpAgA3AwggAkEEakGYisAAIAJBCGoQHiACQSBqJAALWQEBfyMAQSBrIgIkACACIAAoAgA2AgQgAkEYaiABQRBqKQIANwMAIAJBEGogAUEIaikCADcDACACIAEpAgA3AwggAkEEakGAjsAAIAJBCGoQHiACQSBqJAALZwAjAEEwayIBJABB2K3AAC0AAARAIAFBHGpBATYCACABQgI3AgwgAUH0jsAANgIIIAFBAzYCJCABIAA2AiwgASABQSBqNgIYIAEgAUEsajYCICABQQhqQZyPwAAQdAALIAFBMGokAAtZAQF/IwBBIGsiAiQAIAIgACgCADYCBCACQRhqIAFBEGopAgA3AwAgAkEQaiABQQhqKQIANwMAIAIgASkCADcDCCACQQRqQbSRwAAgAkEIahAeIAJBIGokAAtnAQJ/IAEoAgAhAwJAAkACQCABQQhqKAIAIgFFBEBBASECDAELIAFBf0wNASABQQEQngEiAkUNAgsgAiADIAEQuQEhAiAAIAE2AgggACABNgIEIAAgAjYCAA8LEHMACyABQQEQswEAC1YBAX8jAEEgayICJAAgAiAANgIEIAJBGGogAUEQaikCADcDACACQRBqIAFBCGopAgA3AwAgAiABKQIANwMIIAJBBGpBmIrAACACQQhqEB4gAkEgaiQAC1YBAX8CQCAABEAgACgCAA0BIABBfzYCACAAQQhqIgMoAgAEQCAAKAIEEBULIAAgATYCBCAAQQA2AgAgAEEMaiACNgIAIAMgAjYCAA8LEK0BAAsQrgEAC1YBAX8CQCAABEAgACgCAA0BIABBfzYCACAAQRRqIgMoAgAEQCAAKAIQEBULIAAgATYCECAAQQA2AgAgAEEYaiACNgIAIAMgAjYCAA8LEK0BAAsQrgEAC1YBAX8CQCAABEAgACgCAA0BIABBfzYCACAAQTBqIgMoAgAEQCAAKAIsEBULIAAgATYCLCAAQQA2AgAgAEE0aiACNgIAIAMgAjYCAA8LEK0BAAsQrgEAC1YBAX8jAEEQayIFJAAgASgCACACKAIAIAMoAgAgBCgCABAIIQEgBUEIahCDASAFKAIMIQIgACAFKAIIIgNBAEc2AgAgACACIAEgAxs2AgQgBUEQaiQAC08BAn8gACgCACIDQQRqKAIAIANBCGoiBCgCACIAayACSQRAIAMgACACEEEgBCgCACEACyADKAIAIABqIAEgAhC5ARogBCAAIAJqNgIAQQALTwECfyAAKAIAIgNBBGooAgAgA0EIaiIEKAIAIgBrIAJJBEAgAyAAIAIQQiAEKAIAIQALIAMoAgAgAGogASACELkBGiAEIAAgAmo2AgBBAAtRAQF/IwBBEGsiBCQAIAEoAgAgAigCACADKAIAEAchASAEQQhqEIMBIAQoAgwhAiAAIAQoAggiA0EARzYCACAAIAIgASADGzYCBCAEQRBqJAALSgECfyAAQQRqKAIAIABBCGoiBCgCACIDayACSQRAIAAgAyACEEEgBCgCACEDCyAAKAIAIANqIAEgAhC5ARogBCACIANqNgIAQQALPwEBfyMAQSBrIgAkACAAQRxqQQA2AgAgAEGwkMAANgIYIABCATcCDCAAQcyQwAA2AgggAEEIakGkkcAAEHQAC0MBA38CQCACRQ0AA0AgAC0AACIEIAEtAAAiBUYEQCAAQQFqIQAgAUEBaiEBIAJBf2oiAg0BDAILCyAEIAVrIQMLIAMLTAECfyMAQRBrIgMkACABKAIAIAIoAgAQAyEBIANBCGoQgwEgAygCDCECIAAgAygCCCIEQQBHNgIAIAAgAiABIAQbNgIEIANBEGokAAtLAAJAAn8gAUGAgMQARwRAQQEgACgCGCABIABBHGooAgAoAhARAQANARoLIAINAUEACw8LIAAoAhggAiADIABBHGooAgAoAgwRAwALRwEBfyMAQSBrIgIkACACQRRqQQA2AgAgAkGAk8AANgIQIAJCATcCBCACQSs2AhwgAiAANgIYIAIgAkEYajYCACACIAEQdAALRgECfyABKAIEIQIgASgCACEDQQhBBBCeASIBRQRAQQhBBBCzAQALIAEgAjYCBCABIAM2AgAgAEH4j8AANgIEIAAgATYCAAs5AQF/IAFBEHZAACECIABBADYCCCAAQQAgAUGAgHxxIAJBf0YiARs2AgQgAEEAIAJBEHQgARs2AgALZAEDfyMAQRBrIgEkACAAKAIMIgJFBEBBmI7AAEHIj8AAEG8ACyAAKAIIIgNFBEBBmI7AAEHYj8AAEG8ACyABIAI2AgggASAANgIEIAEgAzYCACABKAIAIAEoAgQgASgCCBBQAAs/AQF/IwBBIGsiACQAIABBHGpBADYCACAAQcyRwAA2AhggAEIBNwIMIABBjJLAADYCCCAAQQhqQZSSwAAQdAALPgEBfyMAQSBrIgIkACACQQE6ABggAiABNgIUIAIgADYCECACQfyTwAA2AgwgAkGAk8AANgIIIAJBCGoQcgALKwACQCAAQXxLDQAgAEUEQEEEDwsgACAAQX1JQQJ0EJ4BIgBFDQAgAA8LAAsiACMAQRBrIgAkACAAQQhqIAEQfSAAQQhqEFMgAEEQaiQACysAAkAgAARAIAAoAgANASAAQQA2AgAgAEEcaiABNgIADwsQrQEACxCuAQALKwACQCAABEAgACgCAA0BIABBADYCACAAQSBqIAE2AgAPCxCtAQALEK4BAAsrAAJAIAAEQCAAKAIADQEgAEEANgIAIABBJGogATYCAA8LEK0BAAsQrgEACysAAkAgAARAIAAoAgANASAAQQA2AgAgAEEoaiABNgIADwsQrQEACxCuAQALQAEBfyMAQRBrIgQkACAEIAM2AgwgBCACNgIIIAQgATYCBCAEIAA2AgAgBCgCACAEKAIEIAQoAgggBCgCDBATAAs3ACAAQQM6ACAgAEKAgICAgAQ3AgAgACABNgIYIABBADYCECAAQQA2AgggAEEcakHEh8AANgIACzUBAX8gASgCGEHDjsAAQQsgAUEcaigCACgCDBEDACECIABBADoABSAAIAI6AAQgACABNgIACyUAAkAgAARAIAAoAgBBf0YNASAAQRxqKAIADwsQrQEACxCuAQALJQACQCAABEAgACgCAEF/Rg0BIABBIGooAgAPCxCtAQALEK4BAAslAAJAIAAEQCAAKAIAQX9GDQEgAEEkaigCAA8LEK0BAAsQrgEACyUAAkAgAARAIAAoAgBBf0YNASAAQShqKAIADwsQrQEACxCuAQALJwAgACAAKAIEQQFxIAFyQQJyNgIEIAAgAWoiACAAKAIEQQFyNgIECzoBAn9B3K3AAC0AACEBQdytwABBADoAAEHgrcAAKAIAIQJB4K3AAEEANgIAIAAgAjYCBCAAIAE2AgALIAEBfwJAIAAoAgQiAUUNACAAQQhqKAIARQ0AIAEQFQsLHwACQCABQXxNBEAgACABQQQgAhCZASIADQELAAsgAAsjACACIAIoAgRBfnE2AgQgACABQQFyNgIEIAAgAWogATYCAAslACAARQRAQciMwABBMBCsAQALIAAgAiADIAQgBSABKAIQEQoACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQEQgACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQEQcACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQERUACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQERIACyMAIABFBEBByIzAAEEwEKwBAAsgACACIAMgBCABKAIQERQACx4AIAAgAUEDcjYCBCAAIAFqIgAgACgCBEEBcjYCBAsUACAAQQRqKAIABEAgACgCABAVCwshACAARQRAQciMwABBMBCsAQALIAAgAiADIAEoAhARBAALHwAgAEUEQEHIjMAAQTAQrAEACyAAIAIgASgCEBEBAAsZAQF/IAAoAhAiAQR/IAEFIABBFGooAgALCxkAIAAoAgAiACgCACAAQQhqKAIAIAEQtwELEgBBAEEZIABBAXZrIABBH0YbCxYAIAAgAUEBcjYCBCAAIAFqIAE2AgALHAAgASgCGEHgpsAAQQUgAUEcaigCACgCDBEDAAsTACAAKAIAIgBBJE8EQCAAEAALCxAAIAAgAWpBf2pBACABa3ELFAAgACgCACAAQQhqKAIAIAEQtwELDAAgACABIAIgAxAYCwsAIAEEQCAAEBULCw8AIABBAXQiAEEAIABrcgsUACAAKAIAIAEg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BAAAAAQAAAARAAAAEgAAABMAAABjYW5ub3QgYWNjZXNzIGEgVGhyZWFkIExvY2FsIFN0b3JhZ2UgdmFsdWUgZHVyaW5nIG9yIGFmdGVyIGRlc3RydWN0aW9uL3J1c3RjL2E1NWRkNzFkNWZiMGVjNWE2YTNhOWU4YzI3YjIxMjdiYTQ5MWNlNTIvbGlicmFyeS9zdGQvc3JjL3RocmVhZC9sb2NhbC5ycwAAAHYFEABPAAAApQEAABoAAAAUAAAAAAAAAAEAAAAVAAAAL3J1c3RjL2E1NWRkNzFkNWZiMGVjNWE2YTNhOWU4YzI3YjIxMjdiYTQ5MWNlNTIvbGlicmFyeS9jb3JlL3NyYy9zdHIvcGF0dGVybi5ycwDoBRAATwAAALcBAAAmAAAAY2xvc3VyZSBpbnZva2VkIHJlY3Vyc2l2ZWx5IG9yIGRlc3Ryb3llZCBhbHJlYWR5SnNWYWx1ZSgpAAAAeAYQAAgAAACABhAAAQAAAG51bGwgcG9pbnRlciBwYXNzZWQgdG8gcnVzdHJlY3Vyc2l2ZSB1c2Ugb2YgYW4gb2JqZWN0IGRldGVjdGVkIHdoaWNoIHdvdWxkIGxlYWQgdG8gdW5zYWZlIGFsaWFzaW5nIGluIHJ1c3QAACQAAAAEAAAABAAAACUAAAAmAAAAJwAAAGNhbGxlZCBgT3B0aW9uOjp1bndyYXAoKWAgb24gYSBgTm9uZWAgdmFsdWVBY2Nlc3NFcnJvcm1lbW9yeSBhbGxvY2F0aW9uIG9mICBieXRlcyBmYWlsZWQKAAAATgcQABUAAABjBxAADgAAAGxpYnJhcnkvc3RkL3NyYy9hbGxvYy5yc4QHEAAYAAAAUgEAAAkAAABsaWJyYXJ5L3N0ZC9zcmMvcGFuaWNraW5nLnJzrAcQABwAAABGAgAAHwAAAKwHEAAcAAAARwIAAB4AAAAoAAAADAAAAAQAAAApAAAAJAAAAAgAAAAEAAAAKgAAACsAAAAQAAAABAAAACwAAAAtAAAAJAAAAAgAAAAEAAAALgAAAC8AAABIYXNoIHRhYmxlIGNhcGFjaXR5IG92ZXJmbG93MAgQABwAAAAvY2FyZ28vcmVnaXN0cnkvc3JjL2dpdGh1Yi5jb20tMWVjYzYyOTlkYjllYzgyMy9oYXNoYnJvd24tMC4xMi4zL3NyYy9yYXcvbW9kLnJzAFQIEABPAAAAWgAAACgAAAAwAAAABAAAAAQAAAAxAAAAMgAAADMAAAAwAAAAAAAAAAEAAAAHAAAAbGlicmFyeS9hbGxvYy9zcmMvcmF3X3ZlYy5yc2NhcGFjaXR5IG92ZXJmbG93AAAA+AgQABEAAADcCBAAHAAAAAYCAAAFAAAAYSBmb3JtYXR0aW5nIHRyYWl0IGltcGxlbWVudGF0aW9uIHJldHVybmVkIGFuIGVycm9ybGlicmFyeS9hbGxvYy9zcmMvZm10LnJzAFcJEAAYAAAAZAIAACAAAAAuLgAAgAkQAAIAAABpbmRleCBvdXQgb2YgYm91bmRzOiB0aGUgbGVuIGlzICBidXQgdGhlIGluZGV4IGlzIAAAjAkQACAAAACsCRAAEgAAAGNhbGxlZCBgT3B0aW9uOjp1bndyYXAoKWAgb24gYSBgTm9uZWAgdmFsdWUAOQAAAAAAAAABAAAAOgAAAGA6IACACRAAAAAAAA0KEAACAAAAfSB9MHgwMDAxMDIwMzA0MDUwNjA3MDgwOTEwMTExMjEzMTQxNTE2MTcxODE5MjAyMTIyMjMyNDI1MjYyNzI4MjkzMDMxMzIzMzM0MzUzNjM3MzgzOTQwNDE0MjQzNDQ0NTQ2NDc0ODQ5NTA1MTUyNTM1NDU1NTY1NzU4NTk2MDYxNjI2MzY0NjU2NjY3Njg2OTcwNzE3MjczNzQ3NTc2Nzc3ODc5ODA4MTgyODM4NDg1ODY4Nzg4ODk5MDkxOTI5Mzk0OTU5Njk3OTg5OXJhbmdlIHN0YXJ0IGluZGV4ICBvdXQgb2YgcmFuZ2UgZm9yIHNsaWNlIG9mIGxlbmd0aCAAAADtChAAEgAAAP8KEAAiAAAAbGlicmFyeS9jb3JlL3NyYy9zbGljZS9pbmRleC5ycwA0CxAAHwAAADQAAAAFAAAAcmFuZ2UgZW5kIGluZGV4IGQLEAAQAAAA/woQACIAAAA0CxAAHwAAAEkAAAAFAAAAc2xpY2UgaW5kZXggc3RhcnRzIGF0ICBidXQgZW5kcyBhdCAAlAsQABYAAACqCxAADQAAADQLEAAfAAAAXAAAAAUAAABsaWJyYXJ5L2NvcmUvc3JjL3N0ci9wYXR0ZXJuLnJzANgLEAAfAAAAGgYAABUAAADYCxAAHwAAAEgGAAAVAAAA2AsQAB8AAABJBgAA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"); + +// src/core/parser/Parser.ts +var Parser = class { + async init() { + await rusty_engine_default(rusty_engine_bg_default); + const config = new ParserConfig("<%", "%>", "\0", "*", "-", "_", "tR"); + this.renderer = new Renderer(config); + } + async parse_commands(content, context) { + return this.renderer.render_content(content, context); + } +}; + +// src/core/Templater.ts +var RunMode; +(function(RunMode2) { + RunMode2[RunMode2["CreateNewFromTemplate"] = 0] = "CreateNewFromTemplate"; + RunMode2[RunMode2["AppendActiveFile"] = 1] = "AppendActiveFile"; + RunMode2[RunMode2["OverwriteFile"] = 2] = "OverwriteFile"; + RunMode2[RunMode2["OverwriteActiveFile"] = 3] = "OverwriteActiveFile"; + RunMode2[RunMode2["DynamicProcessor"] = 4] = "DynamicProcessor"; + RunMode2[RunMode2["StartupTemplate"] = 5] = "StartupTemplate"; +})(RunMode || (RunMode = {})); +var Templater = class { + constructor(plugin) { + this.plugin = plugin; + this.functions_generator = new FunctionsGenerator(this.plugin); + this.parser = new Parser(); + } + async setup() { + await this.parser.init(); + await this.functions_generator.init(); + this.plugin.registerMarkdownPostProcessor((el, ctx) => this.process_dynamic_templates(el, ctx)); + } + create_running_config(template_file, target_file, run_mode) { + const active_file = app.workspace.getActiveFile(); + return { + template_file, + target_file, + run_mode, + active_file + }; + } + async read_and_parse_template(config) { + const template_content = await app.vault.read(config.template_file); + return this.parse_template(config, template_content); + } + async parse_template(config, template_content) { + const functions_object = await this.functions_generator.generate_object(config, FunctionsMode.USER_INTERNAL); + this.current_functions_object = functions_object; + const content = await this.parser.parse_commands(template_content, functions_object); + return content; + } + async create_new_note_from_template(template, folder, filename, open_new_note = true) { + if (!folder) { + const new_file_location = app.vault.getConfig("newFileLocation"); + switch (new_file_location) { + case "current": { + const active_file = app.workspace.getActiveFile(); + if (active_file) { + folder = active_file.parent; + } + break; + } + case "folder": + folder = app.fileManager.getNewFileParent(""); + break; + case "root": + folder = app.vault.getRoot(); + break; + default: + break; + } + } + const created_note = await app.fileManager.createNewMarkdownFile(folder, filename ?? "Untitled"); + let running_config; + let output_content; + if (template instanceof import_obsidian13.TFile) { + running_config = this.create_running_config(template, created_note, 0); + output_content = await errorWrapper(async () => this.read_and_parse_template(running_config), "Template parsing error, aborting."); + } else { + running_config = this.create_running_config(void 0, created_note, 0); + output_content = await errorWrapper(async () => this.parse_template(running_config, template), "Template parsing error, aborting."); + } + if (output_content == null) { + await app.vault.delete(created_note); + return; + } + await app.vault.modify(created_note, output_content); + app.workspace.trigger("templater:new-note-from-template", { + file: created_note, + content: output_content + }); + if (open_new_note) { + const active_leaf = app.workspace.getLeaf(false); + if (!active_leaf) { + log_error(new TemplaterError("No active leaf")); + return; + } + await active_leaf.openFile(created_note, { + state: { mode: "source" } + }); + await this.plugin.editor_handler.jump_to_next_cursor_location(created_note, true); + active_leaf.setEphemeralState({ + rename: "all" + }); + } + return created_note; + } + async append_template_to_active_file(template_file) { + const active_view = app.workspace.getActiveViewOfType(import_obsidian13.MarkdownView); + if (active_view === null) { + log_error(new TemplaterError("No active view, can't append templates.")); + return; + } + const running_config = this.create_running_config(template_file, active_view.file, 1); + const output_content = await errorWrapper(async () => this.read_and_parse_template(running_config), "Template parsing error, aborting."); + if (output_content == null) { + return; + } + const editor = active_view.editor; + const doc = editor.getDoc(); + const oldSelections = doc.listSelections(); + doc.replaceSelection(output_content); + app.workspace.trigger("templater:template-appended", { + view: active_view, + content: output_content, + oldSelections, + newSelections: doc.listSelections() + }); + await this.plugin.editor_handler.jump_to_next_cursor_location(active_view.file, true); + } + async write_template_to_file(template_file, file) { + const running_config = this.create_running_config(template_file, file, 2); + const output_content = await errorWrapper(async () => this.read_and_parse_template(running_config), "Template parsing error, aborting."); + if (output_content == null) { + return; + } + await app.vault.modify(file, output_content); + app.workspace.trigger("templater:new-note-from-template", { + file, + content: output_content + }); + await this.plugin.editor_handler.jump_to_next_cursor_location(file, true); + } + overwrite_active_file_commands() { + const active_view = app.workspace.getActiveViewOfType(import_obsidian13.MarkdownView); + if (active_view === null) { + log_error(new TemplaterError("Active view is null, can't overwrite content")); + return; + } + this.overwrite_file_commands(active_view.file, true); + } + async overwrite_file_commands(file, active_file = false) { + const running_config = this.create_running_config(file, file, active_file ? 3 : 2); + const output_content = await errorWrapper(async () => this.read_and_parse_template(running_config), "Template parsing error, aborting."); + if (output_content == null) { + return; + } + await app.vault.modify(file, output_content); + app.workspace.trigger("templater:overwrite-file", { + file, + content: output_content + }); + await this.plugin.editor_handler.jump_to_next_cursor_location(file, true); + } + async process_dynamic_templates(el, ctx) { + const dynamic_command_regex = generate_dynamic_command_regex(); + const walker = document.createNodeIterator(el, NodeFilter.SHOW_TEXT); + let node; + let pass = false; + let functions_object; + while (node = walker.nextNode()) { + let content = node.nodeValue; + if (content !== null) { + let match = dynamic_command_regex.exec(content); + if (match !== null) { + const file = app.metadataCache.getFirstLinkpathDest("", ctx.sourcePath); + if (!file || !(file instanceof import_obsidian13.TFile)) { + return; + } + if (!pass) { + pass = true; + const config = this.create_running_config(file, file, 4); + functions_object = await this.functions_generator.generate_object(config, FunctionsMode.USER_INTERNAL); + this.current_functions_object = functions_object; + } + } + while (match != null) { + const complete_command = match[1] + match[2]; + const command_output = await errorWrapper(async () => { + return await this.parser.parse_commands(complete_command, functions_object); + }, `Command Parsing error in dynamic command '${complete_command}'`); + if (command_output == null) { + return; + } + const start2 = dynamic_command_regex.lastIndex - match[0].length; + const end2 = dynamic_command_regex.lastIndex; + content = content.substring(0, start2) + command_output + content.substring(end2); + dynamic_command_regex.lastIndex += command_output.length - match[0].length; + match = dynamic_command_regex.exec(content); + } + node.nodeValue = content; + } + } + } + get_new_file_template_for_folder(folder) { + do { + const match = this.plugin.settings.folder_templates.find((e) => e.folder == folder.path); + if (match && match.template) { + return match.template; + } + folder = folder.parent; + } while (folder); + } + static async on_file_creation(templater, file) { + if (!(file instanceof import_obsidian13.TFile) || file.extension !== "md") { + return; + } + const template_folder = (0, import_obsidian13.normalizePath)(templater.plugin.settings.templates_folder); + if (file.path.includes(template_folder) && template_folder !== "/") { + return; + } + await delay(300); + if (file.stat.size == 0 && templater.plugin.settings.enable_folder_templates) { + const folder_template_match = templater.get_new_file_template_for_folder(file.parent); + if (!folder_template_match) { + return; + } + const template_file = await errorWrapper(async () => { + return resolve_tfile(folder_template_match); + }, `Couldn't find template ${folder_template_match}`); + if (template_file == null) { + return; + } + await templater.write_template_to_file(template_file, file); + } else { + if (file.stat.size <= 1e5) { + await templater.overwrite_file_commands(file); + } else { + console.log(`Templater skipped parsing ${file.path} because file size exceeds 10000`); + } + } + } + async execute_startup_scripts() { + for (const template of this.plugin.settings.startup_templates) { + if (!template) { + continue; + } + const file = errorWrapperSync(() => resolve_tfile(template), `Couldn't find startup template "${template}"`); + if (!file) { + continue; + } + const running_config = this.create_running_config(file, file, 5); + await errorWrapper(async () => this.read_and_parse_template(running_config), `Startup Template parsing error, aborting.`); + } + } +}; + +// src/handlers/EventHandler.ts +var import_obsidian14 = __toModule(require("obsidian")); +var EventHandler = class { + constructor(plugin, templater, settings) { + this.plugin = plugin; + this.templater = templater; + this.settings = settings; + } + setup() { + app.workspace.onLayoutReady(() => { + this.update_trigger_file_on_creation(); + }); + this.update_syntax_highlighting(); + this.update_file_menu(); + } + update_syntax_highlighting() { + if (this.plugin.settings.syntax_highlighting) { + this.syntax_highlighting_event = app.workspace.on("codemirror", (cm) => { + cm.setOption("mode", "templater"); + }); + app.workspace.iterateCodeMirrors((cm) => { + cm.setOption("mode", "templater"); + }); + this.plugin.registerEvent(this.syntax_highlighting_event); + } else { + if (this.syntax_highlighting_event) { + app.vault.offref(this.syntax_highlighting_event); + } + app.workspace.iterateCodeMirrors((cm) => { + cm.setOption("mode", "hypermd"); + }); + } + } + update_trigger_file_on_creation() { + if (this.settings.trigger_on_file_creation) { + this.trigger_on_file_creation_event = app.vault.on("create", (file) => Templater.on_file_creation(this.templater, file)); + this.plugin.registerEvent(this.trigger_on_file_creation_event); + } else { + if (this.trigger_on_file_creation_event) { + app.vault.offref(this.trigger_on_file_creation_event); + this.trigger_on_file_creation_event = void 0; + } + } + } + update_file_menu() { + this.plugin.registerEvent(app.workspace.on("file-menu", (menu, file) => { + if (file instanceof import_obsidian14.TFolder) { + menu.addItem((item) => { + item.setTitle("Create new note from template").setIcon("templater-icon").onClick(() => { + this.plugin.fuzzy_suggester.create_new_note_from_template(file); + }); + }); + } + })); + } +}; + +// src/handlers/CommandHandler.ts +var CommandHandler = class { + constructor(plugin) { + this.plugin = plugin; + } + setup() { + this.plugin.addCommand({ + id: "insert-templater", + name: "Open Insert Template modal", + hotkeys: [ + { + modifiers: ["Alt"], + key: "e" + } + ], + callback: () => { + this.plugin.fuzzy_suggester.insert_template(); + } + }); + this.plugin.addCommand({ + id: "replace-in-file-templater", + name: "Replace templates in the active file", + hotkeys: [ + { + modifiers: ["Alt"], + key: "r" + } + ], + callback: () => { + this.plugin.templater.overwrite_active_file_commands(); + } + }); + this.plugin.addCommand({ + id: "jump-to-next-cursor-location", + name: "Jump to next cursor location", + hotkeys: [ + { + modifiers: ["Alt"], + key: "Tab" + } + ], + callback: () => { + this.plugin.editor_handler.jump_to_next_cursor_location(); + } + }); + this.plugin.addCommand({ + id: "create-new-note-from-template", + name: "Create new note from template", + hotkeys: [ + { + modifiers: ["Alt"], + key: "n" + } + ], + callback: () => { + this.plugin.fuzzy_suggester.create_new_note_from_template(); + } + }); + this.register_templates_hotkeys(); + } + register_templates_hotkeys() { + this.plugin.settings.enabled_templates_hotkeys.forEach((template) => { + if (template) { + this.add_template_hotkey(null, template); + } + }); + } + add_template_hotkey(old_template, new_template) { + this.remove_template_hotkey(old_template); + if (new_template) { + this.plugin.addCommand({ + id: new_template, + name: `Insert ${new_template}`, + callback: () => { + const template = errorWrapperSync(() => resolve_tfile(new_template), `Couldn't find the template file associated with this hotkey`); + if (!template) { + return; + } + this.plugin.templater.append_template_to_active_file(template); + } + }); + } + } + remove_template_hotkey(template) { + if (template) { + app.commands.removeCommand(`${this.plugin.manifest.id}:${template}`); + } + } +}; + +// src/editor/Editor.ts +var import_obsidian17 = __toModule(require("obsidian")); + +// src/editor/CursorJumper.ts +var import_obsidian15 = __toModule(require("obsidian")); +var CursorJumper = class { + constructor() { + } + async jump_to_next_cursor_location() { + const active_view = app.workspace.getActiveViewOfType(import_obsidian15.MarkdownView); + if (!active_view) { + return; + } + const active_file = active_view.file; + await active_view.save(); + const content = await app.vault.read(active_file); + const { new_content, positions } = this.replace_and_get_cursor_positions(content); + if (positions) { + await app.vault.modify(active_file, new_content); + this.set_cursor_location(positions); + } + if (app.vault.getConfig("vimMode")) { + const cm = active_view.editor.cm.cm; + window.CodeMirrorAdapter.Vim.handleKey(cm, "i", "mapping"); + } + } + get_editor_position_from_index(content, index) { + const substr = content.slice(0, index); + let l = 0; + let offset2 = -1; + let r = -1; + for (; (r = substr.indexOf("\n", r + 1)) !== -1; l++, offset2 = r) + ; + offset2 += 1; + const ch = content.slice(offset2, index).length; + return { line: l, ch }; + } + replace_and_get_cursor_positions(content) { + let cursor_matches = []; + let match; + const cursor_regex = new RegExp("<%\\s*tp.file.cursor\\((?[0-9]{0,2})\\)\\s*%>", "g"); + while ((match = cursor_regex.exec(content)) != null) { + cursor_matches.push(match); + } + if (cursor_matches.length === 0) { + return {}; + } + cursor_matches.sort((m1, m2) => { + return Number(m1.groups && m1.groups["order"]) - Number(m2.groups && m2.groups["order"]); + }); + const match_str = cursor_matches[0][0]; + cursor_matches = cursor_matches.filter((m) => { + return m[0] === match_str; + }); + const positions = []; + let index_offset = 0; + for (const match2 of cursor_matches) { + const index = match2.index - index_offset; + positions.push(this.get_editor_position_from_index(content, index)); + content = content.replace(new RegExp(escape_RegExp(match2[0])), ""); + index_offset += match2[0].length; + if (match2[1] === "") { + break; + } + } + return { new_content: content, positions }; + } + set_cursor_location(positions) { + const active_view = app.workspace.getActiveViewOfType(import_obsidian15.MarkdownView); + if (!active_view) { + return; + } + const editor = active_view.editor; + const selections = []; + for (const pos of positions) { + selections.push({ from: pos }); + } + const transaction = { + selections + }; + editor.transaction(transaction); + } +}; + +// src/editor/Autocomplete.ts +var import_obsidian16 = __toModule(require("obsidian")); + +// toml:/home/runner/work/Templater/Templater/docs/documentation.toml +var tp = { config: { name: "config", description: "This module exposes Templater's running configuration.\n\nThis is mostly useful when writing scripts requiring some context information.\n", functions: { template_file: { name: "template_file", description: "The `TFile` object representing the template file.", definition: "tp.file.template_file" }, target_file: { name: "target_file", description: "The `TFile` object representing the target file where the template will be inserted.", definition: "tp.config.target_file" }, run_mode: { name: "run_mode", description: "The `RunMode`, representing the way Templater was launched (Create new from template, Append to active file, ...)", definition: "tp.config.run_mode" }, active_file: { name: "active_file", description: "The active file (if existing) when launching Templater.", definition: "tp.config.active_file?" } } }, date: { name: "date", description: "This module contains every internal function related to dates.", functions: { now: { name: "now", description: "Retrieves the date.", definition: 'tp.date.now(format: string = "YYYY-MM-DD", offset?: number\u23AEstring, reference?: string, reference_format?: string)', args: { format: { name: "format", description: "Format for the date, refer to [format reference](https://momentjs.com/docs/#/displaying/format/)" }, offset: { name: "offset", description: "Offset for the day, e.g. set this to `-7` to get last week's date. You can also specify the offset as a string using the ISO 8601 format" }, reference: { name: "reference", description: "The date referential, e.g. set this to the note's title" }, reference_format: { name: "reference_format", description: "The date reference format." } } }, tomorrow: { name: "tomorrow", description: "Retrieves tomorrow's date.", definition: 'tp.date.tomorrow(format: string = "YYYY-MM-DD")', args: { format: { name: "format", description: "Format for the date, refer to [format reference](https://momentjs.com/docs/#/displaying/format/)" } } }, yesterday: { name: "yesterday", description: "Retrieves yesterday's date.", definition: 'tp.date.yesterday(format: string = "YYYY-MM-DD")', args: { format: { name: "format", description: "Format for the date, refer to [format reference](https://momentjs.com/docs/#/displaying/format/)" } } }, weekday: { name: "weekday", description: "", definition: 'tp.date.weekday(format: string = "YYYY-MM-DD", weekday: number, reference?: string, reference_format?: string)', args: { format: { name: "format", description: "Format for the date, refer to [format reference](https://momentjs.com/docs/#/displaying/format/)" }, weekday: { name: "weekday", description: "Week day number. If the locale assigns Monday as the first day of the week, `0` will be Monday, `-7` will be last week's day." }, reference: { name: "reference", description: "The date referential, e.g. set this to the note's title" }, reference_format: { name: "reference_format", description: "The date reference format." } } } } }, file: { name: "file", description: "This module contains every internal function related to files.", functions: { content: { name: "content", description: "Retrieves the file's content", definition: "tp.file.content" }, create_new: { name: "create_new", description: "Creates a new file using a specified template or with a specified content.", definition: "tp.file.create_new(template: TFile \u23AE string, filename?: string, open_new: boolean = false, folder?: TFolder)", args: { template: { name: "template", description: "Either the template used for the new file content, or the file content as a string. If it is the template to use, you retrieve it with `tp.file.find_tfile(TEMPLATENAME)`" }, filename: { name: "filename", description: 'The filename of the new file, defaults to "Untitled".' }, open_new: { name: "open_new", description: "Whether to open or not the newly created file. Warning: if you use this option, since commands are executed asynchronously, the file can be opened first and then other commands are appended to that new file and not the previous file." }, folder: { name: "folder", description: 'The folder to put the new file in, defaults to obsidian\'s default location. If you want the file to appear in a different folder, specify it with `app.vault.getAbstractFileByPath("FOLDERNAME")`' } } }, creation_date: { name: "creation_date", description: "Retrieves the file's creation date.", definition: 'tp.file.creation_date(format: string = "YYYY-MM-DD HH:mm")', args: { format: { name: "format", description: "Format for the date, refer to format reference" } } }, cursor: { name: "cursor", description: "Sets the cursor to this location after the template has been inserted. \n\nYou can navigate between the different tp.file.cursor using the configured hotkey in obsidian settings.\n", definition: "tp.file.cursor(order?: number)", args: { order: { name: "order", description: "The order of the different cursors jump, e.g. it will jump from 1 to 2 to 3, and so on.\nIf you specify multiple tp.file.cursor with the same order, the editor will switch to multi-cursor.\n" } } }, cursor_append: { name: "cursor_append", description: "Appends some content after the active cursor in the file.", definition: "tp.file.cursor_append(content: string)", args: { content: { name: "content", description: "The content to append after the active cursor" } } }, exists: { name: "exists", description: "The filename of the file we want to check existence. The fullpath to the file, relative to the Vault and containing the extension, must be provided. e.g. MyFolder/SubFolder/MyFile.", definition: "tp.file.exists(filename: string)", args: { filename: { name: "filename", description: "The filename of the file we want to check existence, e.g. MyFile." } } }, find_tfile: { name: "find_tfile", description: "Search for a file and returns its `TFile` instance", definition: "tp.file.find_tfile(filename: string)", args: { filename: { name: "filename", description: "The filename we want to search and resolve as a `TFile`" } } }, folder: { name: "folder", description: "Retrieves the file's folder name.", definition: "tp.file.folder(relative: boolean = false)", args: { relative: { name: "relative", description: "If set to true, appends the vault relative path to the folder name." } } }, include: { name: "include", description: "Includes the file's link content. Templates in the included content will be resolved.", definition: "tp.file.include(include_link: string \u23AE TFile)", args: { include_link: { name: "include_link", description: "The link to the file to include, e.g. [[MyFile]], or a TFile object. Also supports sections or blocks inclusions, e.g. [[MyFile#Section1]]" } } }, last_modified_date: { name: "last_modified_date", description: "Retrieves the file's last modification date.", definition: 'tp.file.last_modified_date(format: string = "YYYY-MM-DD HH:mm")', args: { format: { name: "format", description: "Format for the date, refer to format reference." } } }, move: { name: "functions.move", description: "Moves the file to the desired vault location.", definition: "tp.file.move(new_path: string, file_to_move?: TFile)", args: { new_path: { name: "new_path", description: "The new vault relative path of the file, without the file extension. Note: the new path needs to include the folder and the filename, e.g. /Notes/MyNote" } } }, path: { name: "path", description: "Retrieves the file's absolute path on the system.", definition: "tp.file.path(relative: boolean = false)", args: { relative: { name: "relative", description: "If set to true, only retrieves the vault's relative path." } } }, rename: { name: "rename", description: "Renames the file (keeps the same file extension).", definition: "tp.file.rename(new_title: string)", args: { new_title: { name: "new_title", description: "The new file title." } } }, selection: { name: "selection", description: "Retrieves the active file's text selection.", definition: "tp.file.selection()" }, tags: { name: "tags", description: "Retrieves the file's tags (array of string)", definition: "tp.file.tags" }, title: { name: "title", definition: "tp.file.title", description: "Retrieves the file's title." } } }, frontmatter: { name: "frontmatter", description: "This modules exposes all the frontmatter variables of a file as variables." }, obsidian: { name: "obsidian", description: "This module exposes all the functions and classes from the obsidian API." }, system: { name: "system", description: "This module contains system related functions.", functions: { clipboard: { name: "clipboard", description: "Retrieves the clipboard's content", definition: "tp.system.clipboard()" }, prompt: { name: "prompt", description: "Spawns a prompt modal and returns the user's input.", definition: "tp.system.prompt(prompt_text?: string, default_value?: string, throw_on_cancel: boolean = false, multiline?: boolean = false)", args: { prompt_text: { name: "prompt_text", description: "Text placed above the input field" }, default_value: { name: "default_value", description: "A default value for the input field" }, throw_on_cancel: { name: "throw_on_cancel", description: "Throws an error if the prompt is canceled, instead of returning a `null` value" }, multiline: { name: "multiline", description: "If set to true, the input field will be a multiline textarea" } } }, suggester: { name: "suggester", description: "Spawns a suggester prompt and returns the user's chosen item.", definition: 'tp.system.suggester(text_items: string[] \u23AE ((item: T) => string), items: T[], throw_on_cancel: boolean = false, placeholder: string = "", limit?: number = undefined)', args: { text_items: { name: "text_items", description: "Array of strings representing the text that will be displayed for each item in the suggester prompt. This can also be a function that maps an item to its text representation." }, items: { name: "items", description: "Array containing the values of each item in the correct order." }, throw_on_cancel: { name: "throw_on_cancel", description: "Throws an error if the prompt is canceled, instead of returning a `null` value" }, placeholder: { name: "placeholder", description: "Placeholder string of the prompt" }, limit: { name: "limit", description: "Limit the number of items rendered at once (useful to improve performance when displaying large lists)" } } } } }, web: { name: "web", description: "This modules contains every internal function related to the web (making web requests).", functions: { daily_quote: { name: "daily_quote", description: "Retrieves and parses the daily quote from the API https://api.quotable.io", definition: "tp.web.daily_quote()" }, random_picture: { name: "random_picture", description: "Gets a random image from https://unsplash.com/", definition: "tp.web.random_picture(size?: string, query?: string, include_size?: boolean)", args: { size: { name: "size", description: "Image size in the format `x`" }, query: { name: "query", description: "Limits selection to photos matching a search term. Multiple search terms can be passed separated by a comma `,`" }, include_dimensions: { name: "include_size", description: "Optional argument to include the specified size in the image link markdown. Defaults to false" } } } } } }; +var documentation_default = { tp }; + +// src/editor/TpDocumentation.ts +var module_names = [ + "config", + "date", + "file", + "frontmatter", + "obsidian", + "system", + "user", + "web" +]; +var module_names_checker = new Set(module_names); +function is_module_name(x) { + return typeof x === "string" && module_names_checker.has(x); +} +function is_function_documentation(x) { + if (x.definition) { + return true; + } + return false; +} +var Documentation = class { + constructor() { + this.documentation = documentation_default; + } + get_all_modules_documentation() { + return Object.values(this.documentation.tp); + } + get_all_functions_documentation(module_name) { + if (!this.documentation.tp[module_name].functions) { + return; + } + return Object.values(this.documentation.tp[module_name].functions); + } + get_module_documentation(module_name) { + return this.documentation.tp[module_name]; + } + get_function_documentation(module_name, function_name) { + return this.documentation.tp[module_name].functions[function_name]; + } + get_argument_documentation(module_name, function_name, argument_name) { + const function_doc = this.get_function_documentation(module_name, function_name); + if (!function_doc || !function_doc.args) { + return null; + } + return function_doc.args[argument_name]; + } +}; + +// src/editor/Autocomplete.ts +var Autocomplete = class extends import_obsidian16.EditorSuggest { + constructor() { + super(app); + this.tp_keyword_regex = /tp\.(?[a-z]*)?(?\.(?[a-z_]*)?)?$/; + this.documentation = new Documentation(); + } + onTrigger(cursor, editor, _file) { + const range = editor.getRange({ line: cursor.line, ch: 0 }, { line: cursor.line, ch: cursor.ch }); + const match = this.tp_keyword_regex.exec(range); + if (!match) { + return null; + } + let query; + const module_name = match.groups && match.groups["module"] || ""; + this.module_name = module_name; + if (match.groups && match.groups["fn_trigger"]) { + if (module_name == "" || !is_module_name(module_name)) { + return null; + } + this.function_trigger = true; + this.function_name = match.groups["fn"] || ""; + query = this.function_name; + } else { + this.function_trigger = false; + query = this.module_name; + } + const trigger_info = { + start: { line: cursor.line, ch: cursor.ch - query.length }, + end: { line: cursor.line, ch: cursor.ch }, + query + }; + this.latest_trigger_info = trigger_info; + return trigger_info; + } + getSuggestions(context) { + let suggestions; + if (this.module_name && this.function_trigger) { + suggestions = this.documentation.get_all_functions_documentation(this.module_name); + } else { + suggestions = this.documentation.get_all_modules_documentation(); + } + if (!suggestions) { + return []; + } + return suggestions.filter((s) => s.name.startsWith(context.query)); + } + renderSuggestion(value, el) { + el.createEl("b", { text: value.name }); + el.createEl("br"); + if (this.function_trigger && is_function_documentation(value)) { + el.createEl("code", { text: value.definition }); + } + if (value.description) { + el.createEl("div", { text: value.description }); + } + } + selectSuggestion(value, _evt) { + const active_view = app.workspace.getActiveViewOfType(import_obsidian16.MarkdownView); + if (!active_view) { + return; + } + active_view.editor.replaceRange(value.name, this.latest_trigger_info.start, this.latest_trigger_info.end); + if (this.latest_trigger_info.start.ch == this.latest_trigger_info.end.ch) { + const cursor_pos = this.latest_trigger_info.end; + cursor_pos.ch += value.name.length; + active_view.editor.setCursor(cursor_pos); + } + } +}; + +// src/editor/mode/javascript.js +(function(mod) { + mod(window.CodeMirror); +})(function(CodeMirror) { + "use strict"; + CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonldMode = parserConfig.jsonld; + var jsonMode = parserConfig.json || jsonldMode; + var trackScope = parserConfig.trackScope !== false; + var isTS = parserConfig.typescript; + var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; + var keywords = function() { + function kw(type2) { + return { type: type2, style: "keyword" }; + } + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); + var operator = kw("operator"), atom = { type: "atom", style: "atom" }; + return { + if: kw("if"), + while: A, + with: A, + else: B, + do: B, + try: B, + finally: B, + return: D, + break: D, + continue: D, + new: kw("new"), + delete: C, + void: C, + throw: C, + debugger: kw("debugger"), + var: kw("var"), + const: kw("var"), + let: kw("var"), + function: kw("function"), + catch: kw("catch"), + for: kw("for"), + switch: kw("switch"), + case: kw("case"), + default: kw("default"), + in: operator, + typeof: operator, + instanceof: operator, + true: atom, + false: atom, + null: atom, + undefined: atom, + NaN: atom, + Infinity: atom, + this: kw("this"), + class: kw("class"), + super: kw("atom"), + yield: C, + export: kw("export"), + import: kw("import"), + extends: C, + await: C + }; + }(); + var isOperatorChar = /[+\-*&%=<>!?|~^@]/; + var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; + function readRegexp(stream) { + var escaped = false, next, inSet = false; + while ((next = stream.next()) != null) { + if (!escaped) { + if (next == "/" && !inSet) + return; + if (next == "[") + inSet = true; + else if (inSet && next == "]") + inSet = false; + } + escaped = !escaped && next == "\\"; + } + } + var type, content; + function ret(tp2, style, cont2) { + type = tp2; + content = cont2; + return style; + } + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { + return ret("number", "number"); + } else if (ch == "." && stream.match("..")) { + return ret("spread", "meta"); + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + return ret(ch); + } else if (ch == "=" && stream.eat(">")) { + return ret("=>", "operator"); + } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { + return ret("number", "number"); + } else if (/\d/.test(ch)) { + stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); + return ret("number", "number"); + } else if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (expressionAllowed(stream, state, 1)) { + readRegexp(stream); + stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); + return ret("regexp", "string-2"); + } else { + stream.eat("="); + return ret("operator", "operator", stream.current()); + } + } else if (ch == "`") { + state.tokenize = tokenQuasi; + return tokenQuasi(stream, state); + } else if (ch == "#" && stream.peek() == "!") { + stream.skipToEnd(); + return ret("meta", "meta"); + } else if (ch == "#" && stream.eatWhile(wordRE)) { + return ret("variable", "property"); + } else if (ch == "<" && stream.match("!--") || ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start))) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (isOperatorChar.test(ch)) { + if (ch != ">" || !state.lexical || state.lexical.type != ">") { + if (stream.eat("=")) { + if (ch == "!" || ch == "=") + stream.eat("="); + } else if (/[<>*+\-|&?]/.test(ch)) { + stream.eat(ch); + if (ch == ">") + stream.eat(ch); + } + } + if (ch == "?" && stream.eat(".")) + return ret("."); + return ret("operator", "operator", stream.current()); + } else if (wordRE.test(ch)) { + stream.eatWhile(wordRE); + var word = stream.current(); + if (state.lastType != ".") { + if (keywords.propertyIsEnumerable(word)) { + var kw = keywords[word]; + return ret(kw.type, kw.style, word); + } + if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false)) + return ret("async", "keyword", word); + } + return ret("variable", "variable", word); + } + } + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next; + if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)) { + state.tokenize = tokenBase; + return ret("jsonld-keyword", "meta"); + } + while ((next = stream.next()) != null) { + if (next == quote && !escaped) + break; + escaped = !escaped && next == "\\"; + } + if (!escaped) + state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = tokenBase; + break; + } + maybeEnd = ch == "*"; + } + return ret("comment", "comment"); + } + function tokenQuasi(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && next == "\\"; + } + return ret("quasi", "string-2", stream.current()); + } + var brackets = "([{}])"; + function findFatArrow(stream, state) { + if (state.fatArrowAt) + state.fatArrowAt = null; + var arrow2 = stream.string.indexOf("=>", stream.start); + if (arrow2 < 0) + return; + if (isTS) { + var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow2)); + if (m) + arrow2 = m.index; + } + var depth = 0, sawSomething = false; + for (var pos = arrow2 - 1; pos >= 0; --pos) { + var ch = stream.string.charAt(pos); + var bracket = brackets.indexOf(ch); + if (bracket >= 0 && bracket < 3) { + if (!depth) { + ++pos; + break; + } + if (--depth == 0) { + if (ch == "(") + sawSomething = true; + break; + } + } else if (bracket >= 3 && bracket < 6) { + ++depth; + } else if (wordRE.test(ch)) { + sawSomething = true; + } else if (/["'\/`]/.test(ch)) { + for (; ; --pos) { + if (pos == 0) + return; + var next = stream.string.charAt(pos - 1); + if (next == ch && stream.string.charAt(pos - 2) != "\\") { + pos--; + break; + } + } + } else if (sawSomething && !depth) { + ++pos; + break; + } + } + if (sawSomething && !depth) + state.fatArrowAt = pos; + } + var atomicTypes = { + atom: true, + number: true, + variable: true, + string: true, + regexp: true, + this: true, + import: true, + "jsonld-keyword": true + }; + function JSLexical(indented, column, type2, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type2; + this.prev = prev; + this.info = info; + if (align != null) + this.align = align; + } + function inScope(state, varname) { + if (!trackScope) + return false; + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) + return true; + for (var cx2 = state.context; cx2; cx2 = cx2.prev) { + for (var v = cx2.vars; v; v = v.next) + if (v.name == varname) + return true; + } + } + function parseJS(state, style, type2, content2, stream) { + var cc = state.cc; + cx.state = state; + cx.stream = stream; + cx.marked = null, cx.cc = cc; + cx.style = style; + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + while (true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type2, content2)) { + while (cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) + return cx.marked; + if (type2 == "variable" && inScope(state, content2)) + return "variable-2"; + return style; + } + } + } + var cx = { state: null, column: null, marked: null, cc: null }; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) + cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function inList(name, list) { + for (var v = list; v; v = v.next) + if (v.name == name) + return true; + return false; + } + function register(varname) { + var state = cx.state; + cx.marked = "def"; + if (!trackScope) + return; + if (state.context) { + if (state.lexical.info == "var" && state.context && state.context.block) { + var newContext = registerVarScoped(varname, state.context); + if (newContext != null) { + state.context = newContext; + return; + } + } else if (!inList(varname, state.localVars)) { + state.localVars = new Var(varname, state.localVars); + return; + } + } + if (parserConfig.globalVars && !inList(varname, state.globalVars)) + state.globalVars = new Var(varname, state.globalVars); + } + function registerVarScoped(varname, context) { + if (!context) { + return null; + } else if (context.block) { + var inner = registerVarScoped(varname, context.prev); + if (!inner) + return null; + if (inner == context.prev) + return context; + return new Context(inner, context.vars, true); + } else if (inList(varname, context.vars)) { + return context; + } else { + return new Context(context.prev, new Var(varname, context.vars), false); + } + } + function isModifier(name) { + return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"; + } + function Context(prev, vars, block2) { + this.prev = prev; + this.vars = vars; + this.block = block2; + } + function Var(name, next) { + this.name = name; + this.next = next; + } + var defaultVars = new Var("this", new Var("arguments", null)); + function pushcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, false); + cx.state.localVars = defaultVars; + } + function pushblockcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, true); + cx.state.localVars = null; + } + function popcontext() { + cx.state.localVars = cx.state.context.vars; + cx.state.context = cx.state.context.prev; + } + popcontext.lex = true; + function pushlex(type2, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") + indent = state.lexical.indented; + else + for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) + indent = outer.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type2, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + function expect(wanted) { + function exp(type2) { + if (type2 == wanted) + return cont(); + else if (wanted == ";" || type2 == "}" || type2 == ")" || type2 == "]") + return pass(); + else + return cont(exp); + } + return exp; + } + function statement(type2, value) { + if (type2 == "var") + return cont(pushlex("vardef", value), vardef, expect(";"), poplex); + if (type2 == "keyword a") + return cont(pushlex("form"), parenExpr, statement, poplex); + if (type2 == "keyword b") + return cont(pushlex("form"), statement, poplex); + if (type2 == "keyword d") + return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); + if (type2 == "debugger") + return cont(expect(";")); + if (type2 == "{") + return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); + if (type2 == ";") + return cont(); + if (type2 == "if") { + if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) + cx.state.cc.pop()(); + return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); + } + if (type2 == "function") + return cont(functiondef); + if (type2 == "for") + return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex); + if (type2 == "class" || isTS && value == "interface") { + cx.marked = "keyword"; + return cont(pushlex("form", type2 == "class" ? type2 : value), className, poplex); + } + if (type2 == "variable") { + if (isTS && value == "declare") { + cx.marked = "keyword"; + return cont(statement); + } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { + cx.marked = "keyword"; + if (value == "enum") + return cont(enumdef); + else if (value == "type") + return cont(typename, expect("operator"), typeexpr, expect(";")); + else + return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex); + } else if (isTS && value == "namespace") { + cx.marked = "keyword"; + return cont(pushlex("form"), expression, statement, poplex); + } else if (isTS && value == "abstract") { + cx.marked = "keyword"; + return cont(statement); + } else { + return cont(pushlex("stat"), maybelabel); + } + } + if (type2 == "switch") + return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, block, poplex, poplex, popcontext); + if (type2 == "case") + return cont(expression, expect(":")); + if (type2 == "default") + return cont(expect(":")); + if (type2 == "catch") + return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); + if (type2 == "export") + return cont(pushlex("stat"), afterExport, poplex); + if (type2 == "import") + return cont(pushlex("stat"), afterImport, poplex); + if (type2 == "async") + return cont(statement); + if (value == "@") + return cont(expression, statement); + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function maybeCatchBinding(type2) { + if (type2 == "(") + return cont(funarg, expect(")")); + } + function expression(type2, value) { + return expressionInner(type2, value, false); + } + function expressionNoComma(type2, value) { + return expressionInner(type2, value, true); + } + function parenExpr(type2) { + if (type2 != "(") + return pass(); + return cont(pushlex(")"), maybeexpression, expect(")"), poplex); + } + function expressionInner(type2, value, noComma) { + if (cx.state.fatArrowAt == cx.stream.start) { + var body = noComma ? arrowBodyNoComma : arrowBody; + if (type2 == "(") + return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); + else if (type2 == "variable") + return pass(pushcontext, pattern, expect("=>"), body, popcontext); + } + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type2)) + return cont(maybeop); + if (type2 == "function") + return cont(functiondef, maybeop); + if (type2 == "class" || isTS && value == "interface") { + cx.marked = "keyword"; + return cont(pushlex("form"), classExpression, poplex); + } + if (type2 == "keyword c" || type2 == "async") + return cont(noComma ? expressionNoComma : expression); + if (type2 == "(") + return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); + if (type2 == "operator" || type2 == "spread") + return cont(noComma ? expressionNoComma : expression); + if (type2 == "[") + return cont(pushlex("]"), arrayLiteral, poplex, maybeop); + if (type2 == "{") + return contCommasep(objprop, "}", null, maybeop); + if (type2 == "quasi") + return pass(quasi, maybeop); + if (type2 == "new") + return cont(maybeTarget(noComma)); + return cont(); + } + function maybeexpression(type2) { + if (type2.match(/[;\}\)\],]/)) + return pass(); + return pass(expression); + } + function maybeoperatorComma(type2, value) { + if (type2 == ",") + return cont(maybeexpression); + return maybeoperatorNoComma(type2, value, false); + } + function maybeoperatorNoComma(type2, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (type2 == "=>") + return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); + if (type2 == "operator") { + if (/\+\+|--/.test(value) || isTS && value == "!") + return cont(me); + if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) + return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); + if (value == "?") + return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type2 == "quasi") { + return pass(quasi, me); + } + if (type2 == ";") + return; + if (type2 == "(") + return contCommasep(expressionNoComma, ")", "call", me); + if (type2 == ".") + return cont(property, me); + if (type2 == "[") + return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + if (isTS && value == "as") { + cx.marked = "keyword"; + return cont(typeexpr, me); + } + if (type2 == "regexp") { + cx.state.lastType = cx.marked = "operator"; + cx.stream.backUp(cx.stream.pos - cx.stream.start - 1); + return cont(expr); + } + } + function quasi(type2, value) { + if (type2 != "quasi") + return pass(); + if (value.slice(value.length - 2) != "${") + return cont(quasi); + return cont(maybeexpression, continueQuasi); + } + function continueQuasi(type2) { + if (type2 == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasi); + } + } + function arrowBody(type2) { + findFatArrow(cx.stream, cx.state); + return pass(type2 == "{" ? statement : expression); + } + function arrowBodyNoComma(type2) { + findFatArrow(cx.stream, cx.state); + return pass(type2 == "{" ? statement : expressionNoComma); + } + function maybeTarget(noComma) { + return function(type2) { + if (type2 == ".") + return cont(noComma ? targetNoComma : target); + else if (type2 == "variable" && isTS) + return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma); + else + return pass(noComma ? expressionNoComma : expression); + }; + } + function target(_, value) { + if (value == "target") { + cx.marked = "keyword"; + return cont(maybeoperatorComma); + } + } + function targetNoComma(_, value) { + if (value == "target") { + cx.marked = "keyword"; + return cont(maybeoperatorNoComma); + } + } + function maybelabel(type2) { + if (type2 == ":") + return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type2) { + if (type2 == "variable") { + cx.marked = "property"; + return cont(); + } + } + function objprop(type2, value) { + if (type2 == "async") { + cx.marked = "property"; + return cont(objprop); + } else if (type2 == "variable" || cx.style == "keyword") { + cx.marked = "property"; + if (value == "get" || value == "set") + return cont(getterSetter); + var m; + if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) + cx.state.fatArrowAt = cx.stream.pos + m[0].length; + return cont(afterprop); + } else if (type2 == "number" || type2 == "string") { + cx.marked = jsonldMode ? "property" : cx.style + " property"; + return cont(afterprop); + } else if (type2 == "jsonld-keyword") { + return cont(afterprop); + } else if (isTS && isModifier(value)) { + cx.marked = "keyword"; + return cont(objprop); + } else if (type2 == "[") { + return cont(expression, maybetype, expect("]"), afterprop); + } else if (type2 == "spread") { + return cont(expressionNoComma, afterprop); + } else if (value == "*") { + cx.marked = "keyword"; + return cont(objprop); + } else if (type2 == ":") { + return pass(afterprop); + } + } + function getterSetter(type2) { + if (type2 != "variable") + return pass(afterprop); + cx.marked = "property"; + return cont(functiondef); + } + function afterprop(type2) { + if (type2 == ":") + return cont(expressionNoComma); + if (type2 == "(") + return pass(functiondef); + } + function commasep(what, end2, sep) { + function proceed(type2, value) { + if (sep ? sep.indexOf(type2) > -1 : type2 == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") + lex.pos = (lex.pos || 0) + 1; + return cont(function(type3, value2) { + if (type3 == end2 || value2 == end2) + return pass(); + return pass(what); + }, proceed); + } + if (type2 == end2 || value == end2) + return cont(); + if (sep && sep.indexOf(";") > -1) + return pass(what); + return cont(expect(end2)); + } + return function(type2, value) { + if (type2 == end2 || value == end2) + return cont(); + return pass(what, proceed); + }; + } + function contCommasep(what, end2, info) { + for (var i = 3; i < arguments.length; i++) + cx.cc.push(arguments[i]); + return cont(pushlex(end2, info), commasep(what, end2), poplex); + } + function block(type2) { + if (type2 == "}") + return cont(); + return pass(statement, block); + } + function maybetype(type2, value) { + if (isTS) { + if (type2 == ":") + return cont(typeexpr); + if (value == "?") + return cont(maybetype); + } + } + function maybetypeOrIn(type2, value) { + if (isTS && (type2 == ":" || value == "in")) + return cont(typeexpr); + } + function mayberettype(type2) { + if (isTS && type2 == ":") { + if (cx.stream.match(/^\s*\w+\s+is\b/, false)) + return cont(expression, isKW, typeexpr); + else + return cont(typeexpr); + } + } + function isKW(_, value) { + if (value == "is") { + cx.marked = "keyword"; + return cont(); + } + } + function typeexpr(type2, value) { + if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") { + cx.marked = "keyword"; + return cont(value == "typeof" ? expressionNoComma : typeexpr); + } + if (type2 == "variable" || value == "void") { + cx.marked = "type"; + return cont(afterType); + } + if (value == "|" || value == "&") + return cont(typeexpr); + if (type2 == "string" || type2 == "number" || type2 == "atom") + return cont(afterType); + if (type2 == "[") + return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType); + if (type2 == "{") + return cont(pushlex("}"), typeprops, poplex, afterType); + if (type2 == "(") + return cont(commasep(typearg, ")"), maybeReturnType, afterType); + if (type2 == "<") + return cont(commasep(typeexpr, ">"), typeexpr); + if (type2 == "quasi") { + return pass(quasiType, afterType); + } + } + function maybeReturnType(type2) { + if (type2 == "=>") + return cont(typeexpr); + } + function typeprops(type2) { + if (type2.match(/[\}\)\]]/)) + return cont(); + if (type2 == "," || type2 == ";") + return cont(typeprops); + return pass(typeprop, typeprops); + } + function typeprop(type2, value) { + if (type2 == "variable" || cx.style == "keyword") { + cx.marked = "property"; + return cont(typeprop); + } else if (value == "?" || type2 == "number" || type2 == "string") { + return cont(typeprop); + } else if (type2 == ":") { + return cont(typeexpr); + } else if (type2 == "[") { + return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop); + } else if (type2 == "(") { + return pass(functiondecl, typeprop); + } else if (!type2.match(/[;\}\)\],]/)) { + return cont(); + } + } + function quasiType(type2, value) { + if (type2 != "quasi") + return pass(); + if (value.slice(value.length - 2) != "${") + return cont(quasiType); + return cont(typeexpr, continueQuasiType); + } + function continueQuasiType(type2) { + if (type2 == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasiType); + } + } + function typearg(type2, value) { + if (type2 == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") + return cont(typearg); + if (type2 == ":") + return cont(typeexpr); + if (type2 == "spread") + return cont(typearg); + return pass(typeexpr); + } + function afterType(type2, value) { + if (value == "<") + return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType); + if (value == "|" || type2 == "." || value == "&") + return cont(typeexpr); + if (type2 == "[") + return cont(typeexpr, expect("]"), afterType); + if (value == "extends" || value == "implements") { + cx.marked = "keyword"; + return cont(typeexpr); + } + if (value == "?") + return cont(typeexpr, expect(":"), typeexpr); + } + function maybeTypeArgs(_, value) { + if (value == "<") + return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType); + } + function typeparam() { + return pass(typeexpr, maybeTypeDefault); + } + function maybeTypeDefault(_, value) { + if (value == "=") + return cont(typeexpr); + } + function vardef(_, value) { + if (value == "enum") { + cx.marked = "keyword"; + return cont(enumdef); + } + return pass(pattern, maybetype, maybeAssign, vardefCont); + } + function pattern(type2, value) { + if (isTS && isModifier(value)) { + cx.marked = "keyword"; + return cont(pattern); + } + if (type2 == "variable") { + register(value); + return cont(); + } + if (type2 == "spread") + return cont(pattern); + if (type2 == "[") + return contCommasep(eltpattern, "]"); + if (type2 == "{") + return contCommasep(proppattern, "}"); + } + function proppattern(type2, value) { + if (type2 == "variable" && !cx.stream.match(/^\s*:/, false)) { + register(value); + return cont(maybeAssign); + } + if (type2 == "variable") + cx.marked = "property"; + if (type2 == "spread") + return cont(pattern); + if (type2 == "}") + return pass(); + if (type2 == "[") + return cont(expression, expect("]"), expect(":"), proppattern); + return cont(expect(":"), pattern, maybeAssign); + } + function eltpattern() { + return pass(pattern, maybeAssign); + } + function maybeAssign(_type, value) { + if (value == "=") + return cont(expressionNoComma); + } + function vardefCont(type2) { + if (type2 == ",") + return cont(vardef); + } + function maybeelse(type2, value) { + if (type2 == "keyword b" && value == "else") + return cont(pushlex("form", "else"), statement, poplex); + } + function forspec(type2, value) { + if (value == "await") + return cont(forspec); + if (type2 == "(") + return cont(pushlex(")"), forspec1, poplex); + } + function forspec1(type2) { + if (type2 == "var") + return cont(vardef, forspec2); + if (type2 == "variable") + return cont(forspec2); + return pass(forspec2); + } + function forspec2(type2, value) { + if (type2 == ")") + return cont(); + if (type2 == ";") + return cont(forspec2); + if (value == "in" || value == "of") { + cx.marked = "keyword"; + return cont(expression, forspec2); + } + return pass(expression, forspec2); + } + function functiondef(type2, value) { + if (value == "*") { + cx.marked = "keyword"; + return cont(functiondef); + } + if (type2 == "variable") { + register(value); + return cont(functiondef); + } + if (type2 == "(") + return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); + if (isTS && value == "<") + return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef); + } + function functiondecl(type2, value) { + if (value == "*") { + cx.marked = "keyword"; + return cont(functiondecl); + } + if (type2 == "variable") { + register(value); + return cont(functiondecl); + } + if (type2 == "(") + return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); + if (isTS && value == "<") + return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl); + } + function typename(type2, value) { + if (type2 == "keyword" || type2 == "variable") { + cx.marked = "type"; + return cont(typename); + } else if (value == "<") { + return cont(pushlex(">"), commasep(typeparam, ">"), poplex); + } + } + function funarg(type2, value) { + if (value == "@") + cont(expression, funarg); + if (type2 == "spread") + return cont(funarg); + if (isTS && isModifier(value)) { + cx.marked = "keyword"; + return cont(funarg); + } + if (isTS && type2 == "this") + return cont(maybetype, maybeAssign); + return pass(pattern, maybetype, maybeAssign); + } + function classExpression(type2, value) { + if (type2 == "variable") + return className(type2, value); + return classNameAfter(type2, value); + } + function className(type2, value) { + if (type2 == "variable") { + register(value); + return cont(classNameAfter); + } + } + function classNameAfter(type2, value) { + if (value == "<") + return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter); + if (value == "extends" || value == "implements" || isTS && type2 == ",") { + if (value == "implements") + cx.marked = "keyword"; + return cont(isTS ? typeexpr : expression, classNameAfter); + } + if (type2 == "{") + return cont(pushlex("}"), classBody, poplex); + } + function classBody(type2, value) { + if (type2 == "async" || type2 == "variable" && (value == "static" || value == "get" || value == "set" || isTS && isModifier(value)) && cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false)) { + cx.marked = "keyword"; + return cont(classBody); + } + if (type2 == "variable" || cx.style == "keyword") { + cx.marked = "property"; + return cont(classfield, classBody); + } + if (type2 == "number" || type2 == "string") + return cont(classfield, classBody); + if (type2 == "[") + return cont(expression, maybetype, expect("]"), classfield, classBody); + if (value == "*") { + cx.marked = "keyword"; + return cont(classBody); + } + if (isTS && type2 == "(") + return pass(functiondecl, classBody); + if (type2 == ";" || type2 == ",") + return cont(classBody); + if (type2 == "}") + return cont(); + if (value == "@") + return cont(expression, classBody); + } + function classfield(type2, value) { + if (value == "!") + return cont(classfield); + if (value == "?") + return cont(classfield); + if (type2 == ":") + return cont(typeexpr, maybeAssign); + if (value == "=") + return cont(expressionNoComma); + var context = cx.state.lexical.prev, isInterface = context && context.info == "interface"; + return pass(isInterface ? functiondecl : functiondef); + } + function afterExport(type2, value) { + if (value == "*") { + cx.marked = "keyword"; + return cont(maybeFrom, expect(";")); + } + if (value == "default") { + cx.marked = "keyword"; + return cont(expression, expect(";")); + } + if (type2 == "{") + return cont(commasep(exportField, "}"), maybeFrom, expect(";")); + return pass(statement); + } + function exportField(type2, value) { + if (value == "as") { + cx.marked = "keyword"; + return cont(expect("variable")); + } + if (type2 == "variable") + return pass(expressionNoComma, exportField); + } + function afterImport(type2) { + if (type2 == "string") + return cont(); + if (type2 == "(") + return pass(expression); + if (type2 == ".") + return pass(maybeoperatorComma); + return pass(importSpec, maybeMoreImports, maybeFrom); + } + function importSpec(type2, value) { + if (type2 == "{") + return contCommasep(importSpec, "}"); + if (type2 == "variable") + register(value); + if (value == "*") + cx.marked = "keyword"; + return cont(maybeAs); + } + function maybeMoreImports(type2) { + if (type2 == ",") + return cont(importSpec, maybeMoreImports); + } + function maybeAs(_type, value) { + if (value == "as") { + cx.marked = "keyword"; + return cont(importSpec); + } + } + function maybeFrom(_type, value) { + if (value == "from") { + cx.marked = "keyword"; + return cont(expression); + } + } + function arrayLiteral(type2) { + if (type2 == "]") + return cont(); + return pass(commasep(expressionNoComma, "]")); + } + function enumdef() { + return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex); + } + function enummember() { + return pass(pattern, maybeAssign); + } + function isContinuedStatement(state, textAfter) { + return state.lastType == "operator" || state.lastType == "," || isOperatorChar.test(textAfter.charAt(0)) || /[,.]/.test(textAfter.charAt(0)); + } + function expressionAllowed(stream, state, backUp) { + return state.tokenize == tokenBase && /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))); + } + return { + startState: function(basecolumn) { + var state = { + tokenize: tokenBase, + lastType: "sof", + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + context: parserConfig.localVars && new Context(null, null, false), + indented: basecolumn || 0 + }; + if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") + state.globalVars = parserConfig.globalVars; + return state; + }, + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + findFatArrow(stream, state); + } + if (state.tokenize != tokenComment && stream.eatSpace()) + return null; + var style = state.tokenize(stream, state); + if (type == "comment") + return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + indent: function(state, textAfter) { + if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) + return CodeMirror.Pass; + if (state.tokenize != tokenBase) + return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top2; + if (!/^\s*else\b/.test(textAfter)) + for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) + lexical = lexical.prev; + else if (c != maybeelse && c != popcontext) + break; + } + while ((lexical.type == "stat" || lexical.type == "form") && (firstChar == "}" || (top2 = state.cc[state.cc.length - 1]) && (top2 == maybeoperatorComma || top2 == maybeoperatorNoComma) && !/^[,\.=+\-*:?[\(]/.test(textAfter))) + lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type2 = lexical.type, closing = firstChar == type2; + if (type2 == "vardef") + return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); + else if (type2 == "form" && firstChar == "{") + return lexical.indented; + else if (type2 == "form") + return lexical.indented + indentUnit; + else if (type2 == "stat") + return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) + return lexical.column + (closing ? 0 : 1); + else + return lexical.indented + (closing ? 0 : indentUnit); + }, + electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + blockCommentContinue: jsonMode ? null : " * ", + lineComment: jsonMode ? null : "//", + fold: "brace", + closeBrackets: "()[]{}''\"\"``", + helperType: jsonMode ? "json" : "javascript", + jsonldMode, + jsonMode, + expressionAllowed, + skipExpression: function(state) { + parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null)); + } + }; + }); + CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); + CodeMirror.defineMIME("text/javascript", "javascript"); + CodeMirror.defineMIME("text/ecmascript", "javascript"); + CodeMirror.defineMIME("application/javascript", "javascript"); + CodeMirror.defineMIME("application/x-javascript", "javascript"); + CodeMirror.defineMIME("application/ecmascript", "javascript"); + CodeMirror.defineMIME("application/json", { + name: "javascript", + json: true + }); + CodeMirror.defineMIME("application/x-json", { + name: "javascript", + json: true + }); + CodeMirror.defineMIME("application/manifest+json", { + name: "javascript", + json: true + }); + CodeMirror.defineMIME("application/ld+json", { + name: "javascript", + jsonld: true + }); + CodeMirror.defineMIME("text/typescript", { + name: "javascript", + typescript: true + }); + CodeMirror.defineMIME("application/typescript", { + name: "javascript", + typescript: true + }); +}); + +// src/editor/mode/custom_overlay.js +(function(mod) { + mod(window.CodeMirror); +})(function(CodeMirror) { + "use strict"; + CodeMirror.customOverlayMode = function(base, overlay, combine) { + return { + startState: function() { + return { + base: CodeMirror.startState(base), + overlay: CodeMirror.startState(overlay), + basePos: 0, + baseCur: null, + overlayPos: 0, + overlayCur: null, + streamSeen: null + }; + }, + copyState: function(state) { + return { + base: CodeMirror.copyState(base, state.base), + overlay: CodeMirror.copyState(overlay, state.overlay), + basePos: state.basePos, + baseCur: null, + overlayPos: state.overlayPos, + overlayCur: null + }; + }, + token: function(stream, state) { + if (stream != state.streamSeen || Math.min(state.basePos, state.overlayPos) < stream.start) { + state.streamSeen = stream; + state.basePos = state.overlayPos = stream.start; + } + if (stream.start == state.basePos) { + state.baseCur = base.token(stream, state.base); + state.basePos = stream.pos; + } + if (stream.start == state.overlayPos) { + stream.pos = stream.start; + state.overlayCur = overlay.token(stream, state.overlay); + state.overlayPos = stream.pos; + } + stream.pos = Math.min(state.basePos, state.overlayPos); + if (state.baseCur && state.overlayCur && state.baseCur.contains("line-HyperMD-codeblock")) { + state.overlayCur = state.overlayCur.replace("line-templater-inline", ""); + state.overlayCur += ` line-background-HyperMD-codeblock-bg`; + } + if (state.overlayCur == null) + return state.baseCur; + else if (state.baseCur != null && state.overlay.combineTokens || combine && state.overlay.combineTokens == null) + return state.baseCur + " " + state.overlayCur; + else + return state.overlayCur; + }, + indent: base.indent && function(state, textAfter, line) { + return base.indent(state.base, textAfter, line); + }, + electricChars: base.electricChars, + innerMode: function(state) { + return { state: state.base, mode: base }; + }, + blankLine: function(state) { + var baseToken, overlayToken; + if (base.blankLine) + baseToken = base.blankLine(state.base); + if (overlay.blankLine) + overlayToken = overlay.blankLine(state.overlay); + return overlayToken == null ? baseToken : combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken; + } + }; + }; +}); + +// src/editor/Editor.ts +var import_language = __toModule(require("@codemirror/language")); +var TP_CMD_TOKEN_CLASS = "templater-command"; +var TP_INLINE_CLASS = "templater-inline"; +var TP_OPENING_TAG_TOKEN_CLASS = "templater-opening-tag"; +var TP_CLOSING_TAG_TOKEN_CLASS = "templater-closing-tag"; +var TP_INTERPOLATION_TAG_TOKEN_CLASS = "templater-interpolation-tag"; +var TP_EXEC_TAG_TOKEN_CLASS = "templater-execution-tag"; +var Editor2 = class { + constructor(plugin) { + this.plugin = plugin; + this.cursor_jumper = new CursorJumper(); + } + async setup() { + await this.registerCodeMirrorMode(); + this.plugin.registerEditorSuggest(new Autocomplete()); + if (import_obsidian17.Platform.isDesktopApp && this.plugin.settings.syntax_highlighting) { + this.plugin.registerEditorExtension(import_language.StreamLanguage.define(window.CodeMirror.getMode({}, { name: "templater" }))); + } + } + async jump_to_next_cursor_location(file = null, auto_jump = false) { + if (auto_jump && !this.plugin.settings.auto_jump_to_cursor) { + return; + } + if (file && app.workspace.getActiveFile() !== file) { + return; + } + await this.cursor_jumper.jump_to_next_cursor_location(); + } + async registerCodeMirrorMode() { + if (!this.plugin.settings.syntax_highlighting) { + return; + } + if (import_obsidian17.Platform.isMobileApp) { + return; + } + const js_mode = window.CodeMirror.getMode({}, "javascript"); + if (js_mode.name === "null") { + log_error(new TemplaterError("Javascript syntax mode couldn't be found, can't enable syntax highlighting.")); + return; + } + const overlay_mode = window.CodeMirror.customOverlayMode; + if (overlay_mode == null) { + log_error(new TemplaterError("Couldn't find customOverlayMode, can't enable syntax highlighting.")); + return; + } + window.CodeMirror.defineMode("templater", function(config) { + const templaterOverlay = { + startState: function() { + const js_state = window.CodeMirror.startState(js_mode); + return { + ...js_state, + inCommand: false, + tag_class: "", + freeLine: false + }; + }, + copyState: function(state) { + const js_state = window.CodeMirror.startState(js_mode); + const new_state = { + ...js_state, + inCommand: state.inCommand, + tag_class: state.tag_class, + freeLine: state.freeLine + }; + return new_state; + }, + blankLine: function(state) { + if (state.inCommand) { + return `line-background-templater-command-bg`; + } + return null; + }, + token: function(stream, state) { + if (stream.sol() && state.inCommand) { + state.freeLine = true; + } + if (state.inCommand) { + let keywords = ""; + if (stream.match(/[-_]{0,1}%>/, true)) { + state.inCommand = false; + state.freeLine = false; + const tag_class = state.tag_class; + state.tag_class = ""; + return `line-${TP_INLINE_CLASS} ${TP_CMD_TOKEN_CLASS} ${TP_CLOSING_TAG_TOKEN_CLASS} ${tag_class}`; + } + const js_result = js_mode.token && js_mode.token(stream, state); + if (stream.peek() == null && state.freeLine) { + keywords += ` line-background-templater-command-bg`; + } + if (!state.freeLine) { + keywords += ` line-${TP_INLINE_CLASS}`; + } + return `${keywords} ${TP_CMD_TOKEN_CLASS} ${js_result}`; + } + const match = stream.match(/<%[-_]{0,1}\s*([*+]{0,1})/, true); + if (match != null) { + switch (match[1]) { + case "*": + state.tag_class = TP_EXEC_TAG_TOKEN_CLASS; + break; + default: + state.tag_class = TP_INTERPOLATION_TAG_TOKEN_CLASS; + break; + } + state.inCommand = true; + return `line-${TP_INLINE_CLASS} ${TP_CMD_TOKEN_CLASS} ${TP_OPENING_TAG_TOKEN_CLASS} ${state.tag_class}`; + } + while (stream.next() != null && !stream.match(/<%/, false)) + ; + return null; + } + }; + return overlay_mode(window.CodeMirror.getMode(config, "hypermd"), templaterOverlay); + }); + } +}; + +// src/main.ts +var TemplaterPlugin = class extends import_obsidian18.Plugin { + async onload() { + await this.load_settings(); + this.templater = new Templater(this); + await this.templater.setup(); + this.editor_handler = new Editor2(this); + await this.editor_handler.setup(); + this.fuzzy_suggester = new FuzzySuggester(this); + this.event_handler = new EventHandler(this, this.templater, this.settings); + this.event_handler.setup(); + this.command_handler = new CommandHandler(this); + this.command_handler.setup(); + (0, import_obsidian18.addIcon)("templater-icon", ICON_DATA); + if (this.settings.enable_ribbon_icon) { + this.addRibbonIcon("templater-icon", "Templater", async () => { + this.fuzzy_suggester.insert_template(); + }).setAttribute("id", "rb-templater-icon"); + } + this.addSettingTab(new TemplaterSettingTab(this)); + app.workspace.onLayoutReady(() => { + this.templater.execute_startup_scripts(); + }); + } + async save_settings() { + await this.saveData(this.settings); + } + async load_settings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } +}; diff --git a/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/manifest.json b/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/manifest.json new file mode 100644 index 0000000..70cf595 --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "templater-obsidian", + "name": "Templater", + "version": "1.16.0", + "description": "Create and use templates", + "minAppVersion": "0.11.13", + "author": "SilentVoid", + "authorUrl": "https://github.com/SilentVoid13", + "isDesktopOnly": false +} diff --git a/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/styles.css b/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/styles.css new file mode 100644 index 0000000..207db95 --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/plugins/templater-obsidian/styles.css @@ -0,0 +1,281 @@ +.templater_search { + width: calc(100% - 20px); +} + +.templater_div { + border-top: 1px solid var(--background-modifier-border); +} + +.templater_div > .setting-item { + border-top: none !important; + align-self: center; +} + +.templater_div > .setting-item > .setting-item-control { + justify-content: space-around; + padding: 0; + width: 100%; +} + +.templater_div + > .setting-item + > .setting-item-control + > .setting-editor-extra-setting-button { + align-self: center; +} + +.templater_donating { + margin: 10px; +} + +.templater_title { + margin: 0; + padding: 0; + margin-top: 5px; + text-align: center; +} + +.templater_template { + align-self: center; + margin-left: 5px; + margin-right: 5px; + width: 70%; +} + +.templater_cmd { + margin-left: 5px; + margin-right: 5px; + font-size: 14px; + width: 100%; +} + +.templater_div2 > .setting-item { + align-content: center; + justify-content: center; +} + +.templater-prompt-div { + display: flex; +} + +.templater-prompt-form { + display: flex; + flex-grow: 1; +} + +.templater-prompt-input { + flex-grow: 1; +} + +.templater-button-div { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 1rem; +} + +textarea.templater-prompt-input { + height: 10rem; +} + +textarea.templater-prompt-input:focus { + border-color: var(--interactive-accent); +} + +.cm-s-obsidian .templater-command-bg { + left: 0px; + right: 0px; + background-color: var(--background-primary-alt); +} + +.cm-s-obsidian .cm-templater-command { + font-size: 0.85em; + font-family: var(--font-monospace); + line-height: 1.3; +} + +.cm-s-obsidian .templater-inline .cm-templater-command { + background-color: var(--background-primary-alt); +} + +.cm-s-obsidian .cm-templater-command.cm-templater-opening-tag { + font-weight: bold; +} + +.cm-s-obsidian .cm-templater-command.cm-templater-closing-tag { + font-weight: bold; +} + +.cm-s-obsidian .cm-templater-command.cm-templater-interpolation-tag { + color: #008bff; +} + +.cm-s-obsidian .cm-templater-command.cm-templater-execution-tag { + color: #c0d700; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-keyword { + color: #00a7aa; + font-weight: normal; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-atom { + color: #f39b35; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-number { + color: #a06fca; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-type { + color: #a06fca; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-def { + color: #98e342; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-property { + color: #d4d4d4; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-variable { + color: #d4d4d4; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-variable-2 { + color: #da7dae; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-variable-3 { + color: #a06fca; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-type.cm-def { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-property.cm-def { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-callee { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-operator { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-qualifier { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-tag { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-tag.cm-bracket { + color: #d4d4d4; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-attribute { + color: #a06fca; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-comment { + color: #696d70; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-comment.cm-tag { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-comment.cm-attribute { + color: #d4d4d4; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-string { + color: #e6db74; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-string-2 { + color: #f39b35; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-meta { + color: #d4d4d4; + background: inherit; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-builtin { + color: #fc4384; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-header { + color: #da7dae; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-hr { + color: #98e342; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-link { + color: #696d70; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.cm-error { + border-bottom: 1px solid #c42412; +} + +.theme-dark .cm-s-obsidian pre.HyperMD-codeblock .cm-keyword { + font-weight: normal; +} + +.theme-dark + .cm-s-obsidian + .cm-templater-command.CodeMirror-activeline-background { + background: #272727; +} + +.theme-dark .cm-s-obsidian .cm-templater-command.CodeMirror-matchingbracket { + outline: 1px solid grey; + color: #d4d4d4 !important; +} + +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); + box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} diff --git a/tests/fixtures/sample_vault/.obsidian/workspace.json b/tests/fixtures/sample_vault/.obsidian/workspace.json new file mode 100644 index 0000000..8a80c3a --- /dev/null +++ b/tests/fixtures/sample_vault/.obsidian/workspace.json @@ -0,0 +1,155 @@ +{ + "main": { + "id": "5f828621a37b21ab", + "type": "split", + "children": [ + { + "id": "2f3322f01c16279b", + "type": "tabs", + "children": [ + { + "id": "d16a705340a291b0", + "type": "leaf", + "state": { + "type": "empty", + "state": {} + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "aeebc2581160842a", + "type": "split", + "children": [ + { + "id": "cd71abd49c3ceb86", + "type": "tabs", + "children": [ + { + "id": "fe51579e3e74af15", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical" + } + } + }, + { + "id": "2a7187c7c8d51306", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + } + } + }, + { + "id": "574f0713150d5067", + "type": "leaf", + "state": { + "type": "starred", + "state": {} + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "b20cf1cec7ad8379", + "type": "split", + "children": [ + { + "id": "e4268aea52a4b751", + "type": "tabs", + "children": [ + { + "id": "495261df1eda8469", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + } + } + }, + { + "id": "a552a9e316c497c2", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "linksCollapsed": false, + "unlinkedCollapsed": true + } + } + }, + { + "id": "49ac9a323fc7a3bb", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true + } + } + }, + { + "id": "93bd91c8147876e4", + "type": "leaf", + "state": { + "type": "outline", + "state": {} + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false, + "templater-obsidian:Templater": false + } + }, + "active": "d16a705340a291b0", + "lastOpenFiles": [ + "00 meta/templates/daily note.md", + "+inbox/Untitled.md", + "01 frontmatter/frontmatter 1.md", + "01 frontmatter/frontmatter 2.md", + "01 frontmatter/frontmatter 3.md", + "01 frontmatter/frontmatter 4.md", + "02 inline/inline 1.md", + "02 inline/inline 2.md", + "02 inline/inline 3.md", + "02 inline/inline 4.md" + ] +} \ No newline at end of file diff --git a/tests/fixtures/sample_vault/00 meta/templates/daily note.md b/tests/fixtures/sample_vault/00 meta/templates/daily note.md new file mode 100644 index 0000000..6f825ee --- /dev/null +++ b/tests/fixtures/sample_vault/00 meta/templates/daily note.md @@ -0,0 +1,19 @@ +<%* let title = tp.file.title + if (title.startsWith("Untitled")) { + title = await tp.system.prompt("Title"); + await tp.file.rename(title); + } +-%> +<%* + let result = title.replace(/-/g, ' ') + result = result.charAt(0).toUpperCase() + result.slice(1); + tR += "---" +%> +title: <%* tR += "\"" + result + "\"" %> +tags: +<% tp.file.cursor(1) %> +programming-languagues: +created: <% tp.date.now("YYYY-MM-DD") %> +--- +# <%* tR += result %> +--- \ No newline at end of file diff --git a/tests/fixtures/sample_vault/00 meta/templates/data sample.md b/tests/fixtures/sample_vault/00 meta/templates/data sample.md new file mode 100644 index 0000000..d838ec1 --- /dev/null +++ b/tests/fixtures/sample_vault/00 meta/templates/data sample.md @@ -0,0 +1,17 @@ +--- +area: +date_created: 2022-12-21 +date_modified: 2022-12-20 +tags: + - food/fruit/apple + - food/fruit/pear + - dinner + - lunch + - breakfast +author: John Doe +status: new +type: +- book +- article +- note +--- diff --git a/tests/fixtures/sample_vault/01 frontmatter/frontmatter 1.md b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 1.md new file mode 100644 index 0000000..5937dc2 --- /dev/null +++ b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 1.md @@ -0,0 +1,255 @@ +--- +area: frontmatter +date_created: 2022-12-22 +date_modified: 2022-12-22 +tags: + - food/fruit/apple + - food/fruit/pear + - dinner + - lunch + - breakfast +thoughts: + rating: 8 + reviewable: false +levels: + level1: + - level1a + - level1b + level2: + - level2a + - level2b +author: John Doe +status: new +type: ["book", "article", "note", "one-off"] +--- +# Page Title H1 + +# Headings + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Heading 2 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +### Heading 3 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +#### Heading 4 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +##### Heading 5 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +###### Heading 6 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Text styles + +Lorem ipsum **dolor sit amet**, consectetur adipisicing elit, sed do _eiusmod tempor incididunt_ ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud [[internal link]] exercitation ullamco laboris nisi ut [external link](https://google.com) aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. [[frontmatter 1]] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum `inline code looks like this` dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. + +Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +```css +This is a code block +``` + +## Bold + +**The quick brown fox jumps over the lazy dog.** +**The quick brown fox jumps over the lazy dog.** +The quick brown fox jumps over the lazy dog. + +## Italic + +_The quick brown fox jumps over the lazy dog._ +_The quick brown fox jumps over the lazy dog._ +The quick brown fox jumps over the lazy dog. + +## Bold and Italic + +**_The quick brown fox jumps over the lazy dog._** +The quick brown fox jumps over the lazy dog. + +## Blockquotes + +> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua + +> The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. + +> **The quick brown fox** _jumps over the lazy dog._ + +## Monospaced + +The quick brown fox jumps over the lazy dog. + +## Underlined + +The quick brown fox jumps over the lazy dog. + +## Strike-through + +~~The quick brown fox jumps over the lazy dog.~~ + +## sub and super + +Subscript The quick brown fox jumps over the lazy dog. +Superscript The quick brown fox jumps over the lazy dog. + +## Syntax Highlighting + +A class method is an instance method of the class object. When a new class is created, an object of type `Class` is initialized and assigned to a global constant (Mobile in this case). + +``` +public static String monthNames[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; +``` + +```java +public static String monthNames[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; +``` + +```css +button.mod-cta a { + color: inherit; + text-decoration: none; +} +``` + +## Tables + +| one | two | three | +| ---- | :--: | ----- | +| 9999 | 9999 | 9999 | +| 1 | 2 | 3 | +| 44 | 55 | 66 | + +| Default | Left align | Center align | Right align | +| ---------- | :--------- | :----------: | ----------: | +| 9999999999 | 9999999999 | 9999999999 | 9999999999 | +| 999999999 | 999999999 | 999999999 | 999999999 | +| 99999999 | 99999999 | 99999999 | 99999999 | +| 9999999 | 9999999 | 9999999 | 9999999 | + +| A | B | C | +| --- | --- | ----------------- | +| 1 | 2 | 3
4
5 | + +## Links + +[The-Ultimate-Markdown-Cheat-Sheet](https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet) +[The-Ultimate-Markdown-Cheat-Sheet][reference text] +[The-Ultimate-Markdown-Cheat-Sheet][1] +[Markdown-Cheat-Sheet] + +[reference text]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet +[1]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet +[markdown-cheat-sheet]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet + +[Example of a relative link](rl.md) +Visit https://github.com/ + +## Images + +![alt text][image] + + + + +## Lists +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +1. One +2. Two +3. Three + +## Multi-level Lists + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +1. First level + 1. Second level + - Third level + - Fourth level +2. First level + 1. Second level +3. First level + 1. Second level + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- 1 +- 2 +- 3 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- First level + - Second level + - Third level + - Fourth level +- First level + - Second level +- First level + - Second level + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- [x] Fix Bug 223 ✅ 2022-08-08 +- [x] Add Feature 33 ✅ 2022-08-08 +- [x] Add unit tests ✅ 2022-08-08 + +## Horizontal Rules + +--- + +--- + +--- + +## Miscellaneous + + + +- Asterisk + \ Backslash + ` Backtick + {} Curly braces + . Dot + ! Exclamation mark + +## Hash symbol + +- Hyphen symbol + () Parentheses + +* Plus symbol + [] Square brackets + \_ Underscore + +\* Asterisk +\\ Backslash +\` Backtick +\{} Curly braces +\. Dot +\! Exclamation mark +\# Hash symbol +\- Hyphen symbol +\() Parentheses +\+ Plus symbol +\[] Square brackets +\_ Underscore + +:octocat: + +@lifeparticle + +\# diff --git a/tests/fixtures/sample_vault/01 frontmatter/frontmatter 2.md b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 2.md new file mode 100644 index 0000000..d26d90b --- /dev/null +++ b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 2.md @@ -0,0 +1,255 @@ +--- +area: frontmatter +date_created: 2022-12-22 +date_modified: 2022-11-14 +tags: + - food/fruit/apple + - food/fruit/pear + - dinner + - lunch + - breakfast +thoughts: + rating: 8 + reviewable: false +levels: + level1: + - level1a + - level1b + level2: + - level2a + - level2b +author: John Doe +status: new +type: ["book", "article", "note"] +--- +# Page Title H1 + +# Headings + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Heading 2 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +### Heading 3 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +#### Heading 4 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +##### Heading 5 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +###### Heading 6 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Text styles + +Lorem ipsum **dolor sit amet**, consectetur adipisicing elit, sed do _eiusmod tempor incididunt_ ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud [[internal link]] exercitation ullamco laboris nisi ut [external link](https://google.com) aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. [[frontmatter 1]] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum `inline code looks like this` dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. + +Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +```css +This is a code block +``` + +## Bold + +**The quick brown fox jumps over the lazy dog.** +**The quick brown fox jumps over the lazy dog.** +The quick brown fox jumps over the lazy dog. + +## Italic + +_The quick brown fox jumps over the lazy dog._ +_The quick brown fox jumps over the lazy dog._ +The quick brown fox jumps over the lazy dog. + +## Bold and Italic + +**_The quick brown fox jumps over the lazy dog._** +The quick brown fox jumps over the lazy dog. + +## Blockquotes + +> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua + +> The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. + +> **The quick brown fox** _jumps over the lazy dog._ + +## Monospaced + +The quick brown fox jumps over the lazy dog. + +## Underlined + +The quick brown fox jumps over the lazy dog. + +## Strike-through + +~~The quick brown fox jumps over the lazy dog.~~ + +## sub and super + +Subscript The quick brown fox jumps over the lazy dog. +Superscript The quick brown fox jumps over the lazy dog. + +## Syntax Highlighting + +A class method is an instance method of the class object. When a new class is created, an object of type `Class` is initialized and assigned to a global constant (Mobile in this case). + +``` +public static String monthNames[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; +``` + +```java +public static String monthNames[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; +``` + +```css +button.mod-cta a { + color: inherit; + text-decoration: none; +} +``` + +## Tables + +| one | two | three | +| ---- | :--: | ----- | +| 9999 | 9999 | 9999 | +| 1 | 2 | 3 | +| 44 | 55 | 66 | + +| Default | Left align | Center align | Right align | +| ---------- | :--------- | :----------: | ----------: | +| 9999999999 | 9999999999 | 9999999999 | 9999999999 | +| 999999999 | 999999999 | 999999999 | 999999999 | +| 99999999 | 99999999 | 99999999 | 99999999 | +| 9999999 | 9999999 | 9999999 | 9999999 | + +| A | B | C | +| --- | --- | ----------------- | +| 1 | 2 | 3
4
5 | + +## Links + +[The-Ultimate-Markdown-Cheat-Sheet](https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet) +[The-Ultimate-Markdown-Cheat-Sheet][reference text] +[The-Ultimate-Markdown-Cheat-Sheet][1] +[Markdown-Cheat-Sheet] + +[reference text]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet +[1]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet +[markdown-cheat-sheet]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet + +[Example of a relative link](rl.md) +Visit https://github.com/ + +## Images + +![alt text][image] + + + + +## Lists +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +1. One +2. Two +3. Three + +## Multi-level Lists + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +1. First level + 1. Second level + - Third level + - Fourth level +2. First level + 1. Second level +3. First level + 1. Second level + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- 1 +- 2 +- 3 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- First level + - Second level + - Third level + - Fourth level +- First level + - Second level +- First level + - Second level + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- [x] Fix Bug 223 ✅ 2022-08-08 +- [x] Add Feature 33 ✅ 2022-08-08 +- [x] Add unit tests ✅ 2022-08-08 + +## Horizontal Rules + +--- + +--- + +--- + +## Miscellaneous + + + +- Asterisk + \ Backslash + ` Backtick + {} Curly braces + . Dot + ! Exclamation mark + +## Hash symbol + +- Hyphen symbol + () Parentheses + +* Plus symbol + [] Square brackets + \_ Underscore + +\* Asterisk +\\ Backslash +\` Backtick +\{} Curly braces +\. Dot +\! Exclamation mark +\# Hash symbol +\- Hyphen symbol +\() Parentheses +\+ Plus symbol +\[] Square brackets +\_ Underscore + +:octocat: + +@lifeparticle + +\# diff --git a/tests/fixtures/sample_vault/01 frontmatter/frontmatter 3.md b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 3.md new file mode 100644 index 0000000..fd4f5bd --- /dev/null +++ b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 3.md @@ -0,0 +1,255 @@ +--- +area: frontmatter +date_created: 2022-12-22 +date_modified: 2022-10-01 +tags: + - food/fruit/apple + - food/fruit/pear + - dinner + - lunch + - breakfast +thoughts: + rating: 8 + reviewable: false +levels: + level1: + - level1a + - level1b + level2: + - level2a + - level2b +author: John Doe +status: new +type: ["book", "article", "note"] +--- +# Page Title H1 + +# Headings + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Heading 2 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +### Heading 3 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +#### Heading 4 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +##### Heading 5 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +###### Heading 6 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Text styles + +Lorem ipsum **dolor sit amet**, consectetur adipisicing elit, sed do _eiusmod tempor incididunt_ ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud [[internal link]] exercitation ullamco laboris nisi ut [external link](https://google.com) aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. [[frontmatter 1]] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Lorem ipsum `inline code looks like this` dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. + +Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +```css +This is a code block +``` + +## Bold + +**The quick brown fox jumps over the lazy dog.** +**The quick brown fox jumps over the lazy dog.** +The quick brown fox jumps over the lazy dog. + +## Italic + +_The quick brown fox jumps over the lazy dog._ +_The quick brown fox jumps over the lazy dog._ +The quick brown fox jumps over the lazy dog. + +## Bold and Italic + +**_The quick brown fox jumps over the lazy dog._** +The quick brown fox jumps over the lazy dog. + +## Blockquotes + +> Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua + +> The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. + +> **The quick brown fox** _jumps over the lazy dog._ + +## Monospaced + +The quick brown fox jumps over the lazy dog. + +## Underlined + +The quick brown fox jumps over the lazy dog. + +## Strike-through + +~~The quick brown fox jumps over the lazy dog.~~ + +## sub and super + +Subscript The quick brown fox jumps over the lazy dog. +Superscript The quick brown fox jumps over the lazy dog. + +## Syntax Highlighting + +A class method is an instance method of the class object. When a new class is created, an object of type `Class` is initialized and assigned to a global constant (Mobile in this case). + +``` +public static String monthNames[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; +``` + +```java +public static String monthNames[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; +``` + +```css +button.mod-cta a { + color: inherit; + text-decoration: none; +} +``` + +## Tables + +| one | two | three | +| ---- | :--: | ----- | +| 9999 | 9999 | 9999 | +| 1 | 2 | 3 | +| 44 | 55 | 66 | + +| Default | Left align | Center align | Right align | +| ---------- | :--------- | :----------: | ----------: | +| 9999999999 | 9999999999 | 9999999999 | 9999999999 | +| 999999999 | 999999999 | 999999999 | 999999999 | +| 99999999 | 99999999 | 99999999 | 99999999 | +| 9999999 | 9999999 | 9999999 | 9999999 | + +| A | B | C | +| --- | --- | ----------------- | +| 1 | 2 | 3
4
5 | + +## Links + +[The-Ultimate-Markdown-Cheat-Sheet](https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet) +[The-Ultimate-Markdown-Cheat-Sheet][reference text] +[The-Ultimate-Markdown-Cheat-Sheet][1] +[Markdown-Cheat-Sheet] + +[reference text]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet +[1]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet +[markdown-cheat-sheet]: https://github.com/lifeparticle/The-Ultimate-Markdown-Cheat-Sheet + +[Example of a relative link](rl.md) +Visit https://github.com/ + +## Images + +![alt text][image] + + + + +## Lists +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +1. One +2. Two +3. Three + +## Multi-level Lists + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +1. First level + 1. Second level + - Third level + - Fourth level +2. First level + 1. Second level +3. First level + 1. Second level + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- 1 +- 2 +- 3 + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- First level + - Second level + - Third level + - Fourth level +- First level + - Second level +- First level + - Second level + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- [x] Fix Bug 223 ✅ 2022-08-08 +- [x] Add Feature 33 ✅ 2022-08-08 +- [x] Add unit tests ✅ 2022-08-08 + +## Horizontal Rules + +--- + +--- + +--- + +## Miscellaneous + + + +- Asterisk + \ Backslash + ` Backtick + {} Curly braces + . Dot + ! Exclamation mark + +## Hash symbol + +- Hyphen symbol + () Parentheses + +* Plus symbol + [] Square brackets + \_ Underscore + +\* Asterisk +\\ Backslash +\` Backtick +\{} Curly braces +\. Dot +\! Exclamation mark +\# Hash symbol +\- Hyphen symbol +\() Parentheses +\+ Plus symbol +\[] Square brackets +\_ Underscore + +:octocat: + +@lifeparticle + +\# diff --git a/tests/fixtures/sample_vault/01 frontmatter/frontmatter 4.md b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 4.md new file mode 100644 index 0000000..bb7ab33 --- /dev/null +++ b/tests/fixtures/sample_vault/01 frontmatter/frontmatter 4.md @@ -0,0 +1,29 @@ +--- +area: frontmatter +date_created: 2022-12-22 +date_modified: 2022-12-22 +tags: + - food/fruit/apple + - food/fruit/pear + - dinner + - lunch + - breakfast +thoughts: + rating: 8 + reviewable: false +levels: + level1: + - level1a + - level1b + level2: + - level2a + - level2b +author: John Doe +status: new +type: ["book", "article", "note"] +something_new_here: I-am-new +--- + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? diff --git a/tests/fixtures/sample_vault/02 inline/inline 1.md b/tests/fixtures/sample_vault/02 inline/inline 1.md new file mode 100644 index 0000000..d48b480 --- /dev/null +++ b/tests/fixtures/sample_vault/02 inline/inline 1.md @@ -0,0 +1,18 @@ + +area:: frontmatter +date_created:: 2022-12-22 +date_modified:: 2022-12-22 +author:: John Doe +status:: new +type:: book +type:: article +#food/fruit/apple +#food/fruit/pear +#dinner #lunch #breakfast + + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. \ No newline at end of file diff --git a/tests/fixtures/sample_vault/02 inline/inline 2.md b/tests/fixtures/sample_vault/02 inline/inline 2.md new file mode 100644 index 0000000..d48b480 --- /dev/null +++ b/tests/fixtures/sample_vault/02 inline/inline 2.md @@ -0,0 +1,18 @@ + +area:: frontmatter +date_created:: 2022-12-22 +date_modified:: 2022-12-22 +author:: John Doe +status:: new +type:: book +type:: article +#food/fruit/apple +#food/fruit/pear +#dinner #lunch #breakfast + + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. \ No newline at end of file diff --git a/tests/fixtures/sample_vault/02 inline/inline 3.md b/tests/fixtures/sample_vault/02 inline/inline 3.md new file mode 100644 index 0000000..d48b480 --- /dev/null +++ b/tests/fixtures/sample_vault/02 inline/inline 3.md @@ -0,0 +1,18 @@ + +area:: frontmatter +date_created:: 2022-12-22 +date_modified:: 2022-12-22 +author:: John Doe +status:: new +type:: book +type:: article +#food/fruit/apple +#food/fruit/pear +#dinner #lunch #breakfast + + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. \ No newline at end of file diff --git a/tests/fixtures/sample_vault/02 inline/inline 4.md b/tests/fixtures/sample_vault/02 inline/inline 4.md new file mode 100644 index 0000000..d48b480 --- /dev/null +++ b/tests/fixtures/sample_vault/02 inline/inline 4.md @@ -0,0 +1,18 @@ + +area:: frontmatter +date_created:: 2022-12-22 +date_modified:: 2022-12-22 +author:: John Doe +status:: new +type:: book +type:: article +#food/fruit/apple +#food/fruit/pear +#dinner #lunch #breakfast + + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. \ No newline at end of file diff --git a/tests/fixtures/sample_vault/03 mixed/mixed 1.md b/tests/fixtures/sample_vault/03 mixed/mixed 1.md new file mode 100644 index 0000000..9ed4690 --- /dev/null +++ b/tests/fixtures/sample_vault/03 mixed/mixed 1.md @@ -0,0 +1,39 @@ +--- +date_created: 2022-12-22 +tags: + - food/fruit/apple + - dinner + - breakfast + - not_food +author: John Doe +nested_list: + nested_list_one: + - nested_list_one_a + - nested_list_one_b +type: +- article +- note +--- + +area:: mixed +date_modified:: 2022-12-22 +status:: new +type:: book +inline_key:: inline_key_value +type:: [[article]] +tags:: from_inline_metadata +**bold_key**:: **bold** key value + + + + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, [in_text_key:: in-text value] eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? #inline_tag + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, #inline_tag2 cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. + +#food/fruit/pear +#food/fruit/orange +#dinner #breakfast +#brunch diff --git a/tests/fixtures/sample_vault/04 no metadata/no_metadata_1.md b/tests/fixtures/sample_vault/04 no metadata/no_metadata_1.md new file mode 100644 index 0000000..8dacce6 --- /dev/null +++ b/tests/fixtures/sample_vault/04 no metadata/no_metadata_1.md @@ -0,0 +1,5 @@ +.lLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repella diff --git a/tests/fixtures/sample_vault_config.toml b/tests/fixtures/sample_vault_config.toml new file mode 100644 index 0000000..208e62f --- /dev/null +++ b/tests/fixtures/sample_vault_config.toml @@ -0,0 +1,8 @@ +vault = "tests/fixtures/sample_vault" + +# folders to ignore when parsing content +exclude_paths = [".git", ".obsidian", "ignore_folder"] + +[metadata] + metadata_location = "frontmatter" # "frontmatter", "top", "bottom" + tags_location = "top" # "frontmatter", "top", "bottom" diff --git a/tests/fixtures/test_vault/ignore_folder/file_to_ignore.md b/tests/fixtures/test_vault/ignore_folder/file_to_ignore.md new file mode 100644 index 0000000..971f792 --- /dev/null +++ b/tests/fixtures/test_vault/ignore_folder/file_to_ignore.md @@ -0,0 +1,39 @@ +--- +date_created: 2022-12-22 +tags: + - shared_tag + - frontmatter_tag1 + - frontmatter_tag2 + - frontmatter_tag3 + - ignored_file_tag1 +author: author name +type: ["article", "note"] +--- +#inline_tag_top1 #inline_tag_top2 +#ignored_file_tag2 + +top_key1:: top_key1_value +top_key2:: top_key2_value + +# Heading 1 +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. #intext_tag1 Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla (#intext_tag2) pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est lab + +```python +#ffffff +# This is sample text with tags and metadata +#in_codeblock_tag1 +#ffffff; +codeblock_key:: some text +The quick brown fox jumped over the #in_codeblock_tag2 +``` + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab `this is #inline_code_tag1` illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? `this is #inline_code_tag2` Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pari + + + +bottom_key1:: bottom_key1_value +bottom_key2:: bottom_key2_value + +#inline_tag_bottom1 +#inline_tag_bottom2 +#shared_tag diff --git a/tests/fixtures/test_vault/no_metadata.md b/tests/fixtures/test_vault/no_metadata.md new file mode 100644 index 0000000..923cf86 --- /dev/null +++ b/tests/fixtures/test_vault/no_metadata.md @@ -0,0 +1,3 @@ +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla paria diff --git a/tests/fixtures/test_vault/test1.md b/tests/fixtures/test_vault/test1.md new file mode 100644 index 0000000..ed3363e --- /dev/null +++ b/tests/fixtures/test_vault/test1.md @@ -0,0 +1,44 @@ +--- +date_created: 2022-12-22 +tags: + - shared_tag + - frontmatter_tag1 + - frontmatter_tag2 + - + - 📅/frontmatter_tag3 +frontmatter_Key1: author name +frontmatter_Key2: ["article", "note"] +shared_key1: shared_key1_value +shared_key2: shared_key2_value1 +--- + +#inline_tag_top1 #inline_tag_top2 + +top_key1:: top_key1_value +**top_key2:: top_key2_value** +top_key3:: [[top_key3_value_as_link]] +shared_key1:: shared_key1_value +shared_key2:: shared_key2_value2 +emoji_📅_key:: emoji_📅_key_value + +# Heading 1 +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. #intext_tag1 Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu [intext_key:: intext_value] fugiat nulla (#intext_tag2) pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est lab + +```python +#ffffff +# This is sample text with tags and metadata +#in_codeblock_tag1 +#ffffff; +codeblock_key:: some text +in_codeblock_key:: in_codeblock_value +The quick brown fox jumped over the #in_codeblock_tag2 +``` + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab `this is #inline_code_tag1` illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? `this is #inline_code_tag2` Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pari + +bottom_key1:: bottom_key1_value +bottom_key2:: bottom_key2_value + +#inline_tag_bottom1 +#inline_tag_bottom2 +#shared_tag diff --git a/tests/fixtures/test_vault_config.toml b/tests/fixtures/test_vault_config.toml new file mode 100644 index 0000000..de72145 --- /dev/null +++ b/tests/fixtures/test_vault_config.toml @@ -0,0 +1,8 @@ +vault = "tests/fixtures/test_vault" + +# folders to ignore when parsing content +exclude_paths = [".git", ".obsidian", "ignore_folder"] + +[metadata] + metadata_location = "frontmatter" # "frontmatter", "top", "bottom" + tags_location = "top" # "frontmatter", "top", "bottom" diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..e574401 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,32 @@ +# type: ignore +"""Helper functions for tests.""" + +import re + + +class Regex: + """Assert that a given string meets some expectations. + + Usage: + from tests.helpers import Regex + + assert caplog.text == Regex(r"^.*$", re.I) + """ + + def __init__(self, pattern, flags=0): + self._regex = re.compile(pattern, flags) + + def __eq__(self, actual): + """Define equality. + + Args: + actual (str): String to be matched to the regex + + Returns: + bool: True if the actual string matches the regex, False otherwise. + """ + return bool(self._regex.search(actual)) + + def __repr__(self): + """Error printed on failed tests.""" + return f"Regex: '{self._regex.pattern}'" diff --git a/tests/metadata_test.py b/tests/metadata_test.py new file mode 100644 index 0000000..3f95ba8 --- /dev/null +++ b/tests/metadata_test.py @@ -0,0 +1,491 @@ +# type: ignore +"""Test metadata.py.""" +from pathlib import Path + +from obsidian_metadata.models.metadata import ( + Frontmatter, + InlineMetadata, + InlineTags, + VaultMetadata, +) +from tests.helpers import Regex + +FILE_CONTENT: str = Path("tests/fixtures/test_vault/test1.md").read_text() +METADATA: dict[str, list[str]] = { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["note", "article"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 2", "tag 1", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + "intext_key": ["intext_key_value"], +} +FRONTMATTER_CONTENT: str = """ +--- +tags: + - tag_1 + - tag_2 + - + - 📅/tag_3 +frontmatter_Key1: "frontmatter_Key1_value" +frontmatter_Key2: ["note", "article"] +shared_key1: "shared_key1_value" +--- +more content + +--- +horizontal: rule +--- +""" +INLINE_CONTENT = """\ +repeated_key:: repeated_key_value1 + +#inline_tag_top1,#inline_tag_top2 +**bold_key1**:: bold_key1_value +**bold_key2:: bold_key2_value** +link_key:: [[link_key_value]] +tag_key:: #tag_key_value +emoji_📅_key:: emoji_📅_key_value +**#bold_tag** + +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. [in_text_key1:: in_text_key1_value] Ut enim ad minim veniam, quis nostrud exercitation [in_text_key2:: in_text_key2_value] ullamco laboris nisi ut aliquip ex ea commodo consequat. #in_text_tag Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +```python +#ffffff +# This is sample text [no_key:: value]with tags and metadata +#in_codeblock_tag1 +#ffffff; +in_codeblock_key:: in_codeblock_value +The quick brown fox jumped over the #in_codeblock_tag2 +``` +repeated_key:: repeated_key_value2 +""" + + +def test_vault_metadata(capsys) -> None: + """Test VaultMetadata class.""" + vm = VaultMetadata() + assert vm.dict == {} + + vm.add_metadata(METADATA) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + + vm.print_keys() + captured = capsys.readouterr() + assert captured.out == Regex(r"frontmatter_Key1 +frontmatter_Key2 +intext_key") + + vm.print_tags() + captured = capsys.readouterr() + assert captured.out == Regex(r"tag 1 +tag 2 +tag 3") + + vm.print_metadata() + captured = capsys.readouterr() + assert captured.out == Regex(r"┃ Keys +┃ Values +┃") + assert captured.out == Regex(r"│ +│ tag 3 +│") + assert captured.out == Regex(r"│ frontmatter_Key1 +│ author name +│") + + new_metadata = {"added_key": ["added_value"], "frontmatter_Key2": ["new_value"]} + vm.add_metadata(new_metadata) + assert vm.dict == { + "added_key": ["added_value"], + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "new_value", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + + +def test_vault_metadata_contains() -> None: + """Test contains method.""" + vm = VaultMetadata() + vm.add_metadata(METADATA) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + + assert vm.contains("frontmatter_Key1") is True + assert vm.contains("frontmatter_Key2", "article") is True + assert vm.contains("frontmatter_Key3") is False + assert vm.contains("frontmatter_Key2", "no value") is False + assert vm.contains("1$", is_regex=True) is True + assert vm.contains("5$", is_regex=True) is False + assert vm.contains("tags", r"\d", is_regex=True) is True + assert vm.contains("tags", r"^\d", is_regex=True) is False + + +def test_vault_metadata_delete() -> None: + """Test delete method.""" + vm = VaultMetadata() + vm.add_metadata(METADATA) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + + assert vm.delete("no key") is False + assert vm.delete("tags", "no value") is False + assert vm.delete("tags", "tag 2") is True + assert vm.dict["tags"] == ["tag 1", "tag 3"] + assert vm.delete("tags") is True + assert "tags" not in vm.dict + + +def test_vault_metadata_rename() -> None: + """Test rename method.""" + vm = VaultMetadata() + vm.add_metadata(METADATA) + assert vm.dict == { + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_key_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value"], + "tags": ["tag 1", "tag 2", "tag 3"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value"], + } + + assert vm.rename("no key", "new key") is False + assert vm.rename("tags", "no tag", "new key") is False + assert vm.rename("tags", "tag 2", "new tag") is True + assert vm.dict["tags"] == ["new tag", "tag 1", "tag 3"] + assert vm.rename("tags", "old_tags") is True + assert vm.dict["old_tags"] == ["new tag", "tag 1", "tag 3"] + assert "tags" not in vm.dict + + +def test_frontmatter_create() -> None: + """Test frontmatter creation.""" + frontmatter = Frontmatter(INLINE_CONTENT) + assert frontmatter.dict == {} + + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.dict == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + assert frontmatter.dict_original == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + +def test_frontmatter_contains() -> None: + """Test frontmatter contains.""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + + assert frontmatter.contains("frontmatter_Key1") is True + assert frontmatter.contains("frontmatter_Key2", "article") is True + assert frontmatter.contains("frontmatter_Key3") is False + assert frontmatter.contains("frontmatter_Key2", "no value") is False + + assert frontmatter.contains(r"\d$", is_regex=True) is True + assert frontmatter.contains(r"^\d", is_regex=True) is False + assert frontmatter.contains("key", r"_\d", is_regex=True) is False + assert frontmatter.contains("key", r"\w\d_", is_regex=True) is True + + +def test_frontmatter_rename() -> None: + """Test frontmatter rename.""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.dict == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.rename("no key", "new key") is False + assert frontmatter.rename("tags", "no tag", "new key") is False + + assert frontmatter.has_changes() is False + assert frontmatter.rename("tags", "tag_2", "new tag") is True + + assert frontmatter.dict["tags"] == ["new tag", "tag_1", "📅/tag_3"] + assert frontmatter.rename("tags", "old_tags") is True + assert frontmatter.dict["old_tags"] == ["new tag", "tag_1", "📅/tag_3"] + assert "tags" not in frontmatter.dict + + assert frontmatter.has_changes() is True + + +def test_frontmatter_delete() -> None: + """Test Frontmatter delete method.""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.dict == { + "frontmatter_Key1": ["frontmatter_Key1_value"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "tags": ["tag_1", "tag_2", "📅/tag_3"], + } + + assert frontmatter.delete("no key") is False + assert frontmatter.delete("tags", "no value") is False + assert frontmatter.delete(r"\d{3}") is False + assert frontmatter.has_changes() is False + assert frontmatter.delete("tags", "tag_2") is True + assert frontmatter.dict["tags"] == ["tag_1", "📅/tag_3"] + assert frontmatter.delete("tags") is True + assert "tags" not in frontmatter.dict + assert frontmatter.has_changes() is True + assert frontmatter.delete("shared_key1", r"\w+") is True + assert frontmatter.dict["shared_key1"] == [] + assert frontmatter.delete(r"\w.tter") is True + assert frontmatter.dict == {"shared_key1": []} + + +def test_frontmatter_yaml_conversion(): + """Test Frontmatter to_yaml method.""" + new_frontmatter: str = """\ +tags: + - tag_1 + - tag_2 + - 📅/tag_3 +frontmatter_Key1: frontmatter_Key1_value +frontmatter_Key2: + - article + - note +shared_key1: shared_key1_value +""" + new_frontmatter_sorted: str = """\ +frontmatter_Key1: frontmatter_Key1_value +frontmatter_Key2: + - article + - note +shared_key1: shared_key1_value +tags: + - tag_1 + - tag_2 + - 📅/tag_3 +""" + frontmatter = Frontmatter(FRONTMATTER_CONTENT) + assert frontmatter.to_yaml() == new_frontmatter + assert frontmatter.to_yaml(sort_keys=True) == new_frontmatter_sorted + + +def test_inline_metadata_create() -> None: + """Test inline metadata creation.""" + inline = InlineMetadata(FRONTMATTER_CONTENT) + assert inline.dict == {} + inline = InlineMetadata(INLINE_CONTENT) + assert inline.dict == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + assert inline.dict_original == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + +def test_inline_contains() -> None: + """Test inline metadata contains method.""" + inline = InlineMetadata(INLINE_CONTENT) + + assert inline.contains("bold_key1") is True + assert inline.contains("bold_key2", "bold_key2_value") is True + assert inline.contains("bold_key3") is False + assert inline.contains("bold_key2", "no value") is False + + assert inline.contains(r"\w{4}_key", is_regex=True) is True + assert inline.contains(r"^\d", is_regex=True) is False + assert inline.contains("1$", r"\d_value", is_regex=True) is True + assert inline.contains("key", r"^\d_value", is_regex=True) is False + + +def test_inline_metadata_rename() -> None: + """Test inline metadata rename.""" + inline = InlineMetadata(INLINE_CONTENT) + assert inline.dict == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + assert inline.rename("no key", "new key") is False + assert inline.rename("repeated_key", "no value", "new key") is False + assert inline.has_changes() is False + assert inline.rename("repeated_key", "repeated_key_value1", "new value") is True + assert inline.dict["repeated_key"] == ["new value", "repeated_key_value2"] + assert inline.rename("repeated_key", "old_key") is True + assert inline.dict["old_key"] == ["new value", "repeated_key_value2"] + assert "repeated_key" not in inline.dict + assert inline.has_changes() is True + + +def test_inline_metadata_delete() -> None: + """Test inline metadata delete.""" + inline = InlineMetadata(INLINE_CONTENT) + assert inline.dict == { + "bold_key1": ["bold_key1_value"], + "bold_key2": ["bold_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "repeated_key": ["repeated_key_value1", "repeated_key_value2"], + "tag_key": ["tag_key_value"], + } + + assert inline.delete("no key") is False + assert inline.delete("repeated_key", "no value") is False + assert inline.has_changes() is False + assert inline.delete("repeated_key", "repeated_key_value1") is True + assert inline.dict["repeated_key"] == ["repeated_key_value2"] + assert inline.delete("repeated_key") is True + assert "repeated_key" not in inline.dict + assert inline.has_changes() is True + assert inline.delete(r"\d{3}") is False + assert inline.delete(r"bold_key\d") is True + assert inline.dict == { + "emoji_📅_key": ["emoji_📅_key_value"], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "tag_key": ["tag_key_value"], + } + assert inline.delete("emoji_📅_key", ".*📅.*") is True + assert inline.dict == { + "emoji_📅_key": [], + "in_text_key1": ["in_text_key1_value"], + "in_text_key2": ["in_text_key2_value"], + "link_key": ["link_key_value"], + "tag_key": ["tag_key_value"], + } + + +def test_inline_tags_create() -> None: + """Test inline tags creation.""" + tags = InlineTags(FRONTMATTER_CONTENT) + tags.metadata_key + assert tags.list == [] + + tags = InlineTags(INLINE_CONTENT) + assert tags.list == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + assert tags.list_original == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + + +def test_inline_tags_contains() -> None: + """Test inline tags contains.""" + tags = InlineTags(INLINE_CONTENT) + assert tags.contains("bold_tag") is True + assert tags.contains("no tag") is False + + assert tags.contains(r"\w_\w", is_regex=True) is True + assert tags.contains(r"\d_\d", is_regex=True) is False + + +def test_inline_tags_rename() -> None: + """Test inline tags rename.""" + tags = InlineTags(INLINE_CONTENT) + assert tags.list == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + + assert tags.rename("no tag", "new tag") is False + assert tags.has_changes() is False + assert tags.rename("bold_tag", "new tag") is True + assert tags.list == [ + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "new tag", + "tag_key_value", + ] + assert tags.has_changes() is True + + +def test_inline_tags_delete() -> None: + """Test inline tags delete.""" + tags = InlineTags(INLINE_CONTENT) + assert tags.list == [ + "bold_tag", + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + + assert tags.delete("no tag") is False + assert tags.has_changes() is False + assert tags.delete("bold_tag") is True + assert tags.list == [ + "in_text_tag", + "inline_tag_top1", + "inline_tag_top2", + "tag_key_value", + ] + assert tags.has_changes() is True + assert tags.delete(r"\d{3}") is False + assert tags.delete(r"inline_tag_top\d") is True + assert tags.list == ["in_text_tag", "tag_key_value"] diff --git a/tests/notes_test.py b/tests/notes_test.py new file mode 100644 index 0000000..94e4127 --- /dev/null +++ b/tests/notes_test.py @@ -0,0 +1,358 @@ +# type: ignore +"""Test notes.py.""" + +import re +from pathlib import Path + +import pytest +import typer + +from obsidian_metadata.models.notes import Note +from tests.helpers import Regex + + +def test_note_not_exists() -> None: + """Test target not found.""" + with pytest.raises(typer.Exit): + note = Note(note_path="nonexistent_file.md") + + assert note.note_path == "tests/test_data/test_note.md" + assert note.file_content == "This is a test note." + assert note.frontmatter == {} + assert note.inline_tags == [] + assert note.inline_metadata == {} + assert note.dry_run is False + + +def test_note_create(sample_note) -> None: + """Test creating note class.""" + note = Note(note_path=sample_note, dry_run=True) + assert note.note_path == Path(sample_note) + + assert note.dry_run is True + assert "Lorem ipsum dolor" in note.file_content + assert note.frontmatter.dict == { + "date_created": ["2022-12-22"], + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value1"], + "tags": [ + "frontmatter_tag1", + "frontmatter_tag2", + "shared_tag", + "📅/frontmatter_tag3", + ], + } + + assert note.inline_tags.list == [ + "inline_tag_bottom1", + "inline_tag_bottom2", + "inline_tag_top1", + "inline_tag_top2", + "intext_tag1", + "intext_tag2", + "shared_tag", + ] + assert note.inline_metadata.dict == { + "bottom_key1": ["bottom_key1_value"], + "bottom_key2": ["bottom_key2_value"], + "emoji_📅_key": ["emoji_📅_key_value"], + "intext_key": ["intext_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value2"], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value_as_link"], + } + + with sample_note.open(): + content = sample_note.read_text() + + assert note.file_content == content + assert note.original_file_content == content + + +def test_append(sample_note) -> None: + """Test appending to note.""" + note = Note(note_path=sample_note) + assert note.dry_run is False + + string = "This is a test string." + string2 = "Lorem ipsum dolor sit" + + note.append(string_to_append=string) + assert string in note.file_content + assert len(re.findall(re.escape(string), note.file_content)) == 1 + + note.append(string_to_append=string) + assert string in note.file_content + assert len(re.findall(re.escape(string), note.file_content)) == 1 + + note.append(string_to_append=string, allow_multiple=True) + assert string in note.file_content + assert len(re.findall(re.escape(string), note.file_content)) == 2 + + note.append(string_to_append=string2) + assert string2 in note.file_content + assert len(re.findall(re.escape(string2), note.file_content)) == 1 + + note.append(string_to_append=string2, allow_multiple=True) + assert string2 in note.file_content + assert len(re.findall(re.escape(string2), note.file_content)) == 2 + + +def test_contains_inline_tag(sample_note) -> None: + """Test contains inline tag.""" + note = Note(note_path=sample_note) + assert note.contains_inline_tag("intext_tag1") is True + assert note.contains_inline_tag("nonexistent_tag") is False + assert note.contains_inline_tag(r"\d$", is_regex=True) is True + assert note.contains_inline_tag(r"^\d", is_regex=True) is False + + +def test_contains_metadata(sample_note) -> None: + """Test contains metadata.""" + note = Note(note_path=sample_note) + + assert note.contains_metadata("no key") is False + assert note.contains_metadata("frontmatter_Key2") is True + assert note.contains_metadata(r"^\d", is_regex=True) is False + assert note.contains_metadata(r"^[\w_]+\d", is_regex=True) is True + assert note.contains_metadata("frontmatter_Key2", "no value") is False + assert note.contains_metadata("frontmatter_Key2", "article") is True + assert note.contains_metadata("bottom_key1", "bottom_key1_value") is True + assert note.contains_metadata(r"bottom_key\d$", r"bottom_key\d_value", is_regex=True) is True + + +def test_delete_inline_metadata(sample_note) -> None: + """Test deleting inline metadata.""" + note = Note(note_path=sample_note) + + note._delete_inline_metadata("nonexistent_key") + assert note.file_content == note.original_file_content + note._delete_inline_metadata("frontmatter_Key1") + assert note.file_content == note.original_file_content + + note._delete_inline_metadata("intext_key") + assert note.file_content == Regex(r"dolore eu fugiat", re.DOTALL) + + note._delete_inline_metadata("bottom_key2", "bottom_key2_value") + assert note.file_content != Regex(r"bottom_key2_value") + assert note.file_content == Regex(r"bottom_key2::") + note._delete_inline_metadata("bottom_key1") + assert note.file_content != Regex(r"bottom_key1::") + + +def test_delete_inline_tag(sample_note) -> None: + """Test deleting inline tags.""" + note = Note(note_path=sample_note) + + assert note.delete_inline_tag("not_a_tag") is False + assert note.delete_inline_tag("intext_tag[1]") is True + assert "intext_tag1" not in note.inline_tags.list + assert note.file_content == Regex("consequat. Duis") + + +def test_delete_metadata(sample_note) -> Note: + """Test deleting metadata.""" + note = Note(note_path=sample_note) + + assert note.delete_metadata("nonexistent_key") is False + assert note.delete_metadata("frontmatter_Key1", "no value") is False + assert note.delete_metadata("frontmatter_Key1") is True + assert "frontmatter_Key1" not in note.frontmatter.dict + + assert note.delete_metadata("frontmatter_Key2", "article") is True + assert note.frontmatter.dict["frontmatter_Key2"] == ["note"] + + assert note.delete_metadata("bottom_key1", "bottom_key1_value") is True + assert note.inline_metadata.dict["bottom_key1"] == [] + assert note.file_content == Regex(r"bottom_key1::\n") + + assert note.delete_metadata("bottom_key2") is True + assert "bottom_key2" not in note.inline_metadata.dict + assert note.file_content != Regex(r"bottom_key2") + + +def test_has_changes(sample_note) -> None: + """Test has changes.""" + note = Note(note_path=sample_note) + + assert note.has_changes() is False + note.append("This is a test string.") + assert note.has_changes() is True + + note = Note(note_path=sample_note) + assert note.has_changes() is False + note.delete_metadata("frontmatter_Key1") + assert note.has_changes() is True + + note = Note(note_path=sample_note) + assert note.has_changes() is False + note.delete_metadata("bottom_key2") + assert note.has_changes() is True + + note = Note(note_path=sample_note) + assert note.has_changes() is False + note.delete_inline_tag("intext_tag1") + assert note.has_changes() is True + + +def test_print_note(sample_note, capsys) -> None: + """Test printing note.""" + note = Note(note_path=sample_note) + note.print_note() + captured = capsys.readouterr() + assert "```python" in captured.out + assert "---" in captured.out + assert "#shared_tag" in captured.out + + +def test_print_diff(sample_note, capsys) -> None: + """Test printing diff.""" + note = Note(note_path=sample_note) + note.print_diff() + captured = capsys.readouterr() + assert captured.out == "" + + note.append("This is a test string.") + note.print_diff() + captured = capsys.readouterr() + assert "+ This is a test string." in captured.out + + note.sub("The quick brown fox", "The quick brown hedgehog") + note.print_diff() + captured = capsys.readouterr() + assert "- The quick brown fox" in captured.out + assert "+ The quick brown hedgehog" in captured.out + + +def test_sub(sample_note) -> None: + """Test substituting text in a note.""" + note = Note(note_path=sample_note) + note.sub("#shared_tag", "#unshared_tags", is_regex=True) + assert note.file_content != Regex(r"#shared_tag") + assert note.file_content == Regex(r"#unshared_tags") + + note.sub(" ut ", "") + assert note.file_content != Regex(r" ut ") + assert note.file_content == Regex(r"laboriosam, nisialiquid ex ea") + + +def test_rename_inline_tag(sample_note) -> None: + """Test renaming an inline tag.""" + note = Note(note_path=sample_note) + + assert note.rename_inline_tag("no_note_tag", "intext_tag2") is False + assert note.rename_inline_tag("intext_tag1", "intext_tag26") is True + assert note.inline_tags.list == [ + "inline_tag_bottom1", + "inline_tag_bottom2", + "inline_tag_top1", + "inline_tag_top2", + "intext_tag2", + "intext_tag26", + "shared_tag", + ] + assert note.file_content == Regex(r"#intext_tag26") + assert note.file_content != Regex(r"#intext_tag1") + + +def test_rename_inline_metadata(sample_note) -> None: + """Test renaming inline metadata.""" + note = Note(note_path=sample_note) + + note._rename_inline_metadata("nonexistent_key", "new_key") + assert note.file_content == note.original_file_content + note._rename_inline_metadata("bottom_key1", "no_value", "new_value") + assert note.file_content == note.original_file_content + + note._rename_inline_metadata("bottom_key1", "new_key") + assert note.file_content != Regex(r"bottom_key1::") + assert note.file_content == Regex(r"new_key::") + + note._rename_inline_metadata("emoji_📅_key", "emoji_📅_key_value", "new_value") + assert note.file_content != Regex(r"emoji_📅_key:: ?emoji_📅_key_value") + assert note.file_content == Regex(r"emoji_📅_key:: ?new_value") + + +def test_rename_metadata(sample_note) -> None: + """Test renaming metadata.""" + note = Note(note_path=sample_note) + + assert note.rename_metadata("nonexistent_key", "new_key") is False + assert note.rename_metadata("frontmatter_Key1", "nonexistent_value", "article") is False + + assert note.rename_metadata("frontmatter_Key1", "new_key") is True + assert "frontmatter_Key1" not in note.frontmatter.dict + assert "new_key" in note.frontmatter.dict + assert note.frontmatter.dict["new_key"] == ["author name"] + assert note.file_content == Regex(r"new_key: author name") + + assert note.rename_metadata("frontmatter_Key2", "article", "new_key") is True + assert note.frontmatter.dict["frontmatter_Key2"] == ["new_key", "note"] + assert note.file_content == Regex(r" - new_key") + assert note.file_content != Regex(r" - article") + + assert note.rename_metadata("bottom_key1", "new_key") is True + assert "bottom_key1" not in note.inline_metadata.dict + assert "new_key" in note.inline_metadata.dict + assert note.file_content == Regex(r"new_key:: bottom_key1_value") + + assert note.rename_metadata("new_key", "bottom_key1_value", "new_value") is True + assert note.inline_metadata.dict["new_key"] == ["new_value"] + assert note.file_content == Regex(r"new_key:: new_value") + + +def test_replace_frontmatter(sample_note) -> None: + """Test replacing frontmatter.""" + note = Note(note_path=sample_note) + + note.rename_metadata("frontmatter_Key1", "author name", "some_new_key_here") + note.replace_frontmatter() + new_frontmatter = """--- +date_created: '2022-12-22' +tags: + - frontmatter_tag1 + - frontmatter_tag2 + - shared_tag + - 📅/frontmatter_tag3 +frontmatter_Key1: some_new_key_here +frontmatter_Key2: + - article + - note +shared_key1: shared_key1_value +shared_key2: shared_key2_value1 +---""" + assert new_frontmatter in note.file_content + assert "# Heading 1" in note.file_content + assert "```python" in note.file_content + + note2 = Note(note_path="tests/fixtures/test_vault/no_metadata.md") + note2.replace_frontmatter() + note2.frontmatter.dict = {"key1": "value1", "key2": "value2"} + note2.replace_frontmatter() + new_frontmatter = """--- +key1: value1 +key2: value2 +---""" + assert new_frontmatter in note2.file_content + assert "Lorem ipsum dolor sit amet" in note2.file_content + + +def test_write(sample_note, tmp_path) -> None: + """Test writing note to file.""" + note = Note(note_path=sample_note) + note.sub(pattern="Heading 1", replacement="Heading 2") + + note.write() + note = Note(note_path=sample_note) + assert "Heading 2" in note.file_content + assert "Heading 1" not in note.file_content + + new_path = Path(tmp_path / "new_note.md") + note.write(new_path) + note2 = Note(note_path=new_path) + assert "Heading 2" in note2.file_content + assert "Heading 1" not in note2.file_content diff --git a/tests/patterns_test.py b/tests/patterns_test.py new file mode 100644 index 0000000..c00c11e --- /dev/null +++ b/tests/patterns_test.py @@ -0,0 +1,112 @@ +# type: ignore +"""Tests for the regex module.""" + +import pytest + +from obsidian_metadata.models.patterns import Patterns + +TAG_CONTENT: str = "#1 #2 **#3** [[#4]] [[#5|test]] #6#notag #7_8 #9/10 #11-12 #13; #14, #15. #16: #17* #18(#19) #20[#21] #22\\ #23& #24# #25 **#26** #📅/tag" +INLINE_METADATA: str = """ +**1:: 1** +2_2:: [[2_2]] | 2 +asdfasdf [3:: 3] asdfasdf [7::7] asdf +[4:: 4] [5:: 5] +> 6:: 6 +**8**:: **8** +10:: +📅11:: 11/📅/11 +emoji_📅_key:: 📅emoji_📅_key_value + """ +FRONTMATTER_CONTENT: str = """ +--- +tags: + - tag_1 + - tag_2 + - + - 📅/tag_3 +frontmatter_Key1: "frontmatter_Key1_value" +frontmatter_Key2: ["note", "article"] +shared_key1: 'shared_key1_value' +--- +more content + +--- +horizontal: rule +--- +""" +CORRECT_FRONTMATTER_WITH_SEPARATORS: str = """--- +tags: + - tag_1 + - tag_2 + - + - 📅/tag_3 +frontmatter_Key1: "frontmatter_Key1_value" +frontmatter_Key2: ["note", "article"] +shared_key1: 'shared_key1_value' +---""" +CORRECT_FRONTMATTER_NO_SEPARATORS: str = """ +tags: + - tag_1 + - tag_2 + - + - 📅/tag_3 +frontmatter_Key1: "frontmatter_Key1_value" +frontmatter_Key2: ["note", "article"] +shared_key1: 'shared_key1_value' +""" + + +def test_regex(): + """Test regexes.""" + pattern = Patterns() + + assert pattern.find_inline_tags.findall(TAG_CONTENT) == [ + "1", + "2", + "3", + "4", + "5", + "6", + "7_8", + "9/10", + "11-12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "📅/tag", + ] + + result = pattern.find_inline_metadata.findall(INLINE_METADATA) + assert result == [ + ("", "", "1", "1**"), + ("", "", "2_2", "[[2_2]] | 2"), + ("3", "3", "", ""), + ("7", "7", "", ""), + ("", "", "4", "4] [5:: 5]"), + ("", "", "8**", "**8**"), + ("", "", "11", "11/📅/11"), + ("", "", "emoji_📅_key", "📅emoji_📅_key_value"), + ] + + found = pattern.frontmatt_block_with_separators.search(FRONTMATTER_CONTENT).group("frontmatter") + assert found == CORRECT_FRONTMATTER_WITH_SEPARATORS + + found = pattern.frontmatt_block_no_separators.search(FRONTMATTER_CONTENT).group("frontmatter") + assert found == CORRECT_FRONTMATTER_NO_SEPARATORS + + with pytest.raises(AttributeError): + pattern.frontmatt_block_no_separators.search(TAG_CONTENT).group("frontmatter") + + assert pattern.validate_tag_text.search("test_tag") is None + assert pattern.validate_tag_text.search("#asdf").group(0) == "#" diff --git a/tests/utilities_test.py b/tests/utilities_test.py new file mode 100644 index 0000000..2ea83ed --- /dev/null +++ b/tests/utilities_test.py @@ -0,0 +1,116 @@ +# type: ignore +"""Test the utilities module.""" + + +from obsidian_metadata._utils import ( + clean_dictionary, + dict_contains, + dict_values_to_lists_strings, + remove_markdown_sections, + vault_validation, +) + + +def test_dict_contains() -> None: + """Test dict_contains.""" + d = {"key1": ["value1", "value2"], "key2": ["value3", "value4"], "key3": ["value5", "value6"]} + + assert dict_contains(d, "key1") is True + assert dict_contains(d, "key5") is False + assert dict_contains(d, "key1", "value1") is True + assert dict_contains(d, "key1", "value5") is False + assert dict_contains(d, "key[1-2]", is_regex=True) is True + assert dict_contains(d, "^1", is_regex=True) is False + assert dict_contains(d, r"key\d", r"value\d", is_regex=True) is True + assert dict_contains(d, "key1$", "^alue", is_regex=True) is False + assert dict_contains(d, r"key\d", "value5", is_regex=True) is True + + +def test_dict_values_to_lists_strings(): + """Test converting dictionary values to lists of strings.""" + dictionary = { + "key1": "value1", + "key2": ["value2", "value3", None], + "key3": {"key4": "value4"}, + "key5": {"key6": {"key7": "value7"}}, + "key6": None, + "key8": [1, 3, None, 4], + "key9": [None, "", "None"], + "key10": "None", + "key11": "", + } + + result = dict_values_to_lists_strings(dictionary) + assert result == { + "key1": ["value1"], + "key10": ["None"], + "key11": [""], + "key2": ["None", "value2", "value3"], + "key3": {"key4": ["value4"]}, + "key5": {"key6": {"key7": ["value7"]}}, + "key6": ["None"], + "key8": ["1", "3", "4", "None"], + "key9": ["", "None", "None"], + } + + result = dict_values_to_lists_strings(dictionary, strip_null_values=True) + assert result == { + "key1": ["value1"], + "key10": [], + "key11": [], + "key2": ["value2", "value3"], + "key3": {"key4": ["value4"]}, + "key5": {"key6": {"key7": ["value7"]}}, + "key6": [], + "key8": ["1", "3", "4"], + "key9": ["", "None"], + } + + +def test_vault_validation(): + """Test vault validation.""" + assert vault_validation("tests/") is True + assert "Path is not a directory" in vault_validation("pyproject.toml") + assert "Path does not exist" in vault_validation("tests/vault2") + + +def test_remove_markdown_sections(): + """Test removing markdown sections.""" + text: str = """ +--- +key: value +--- + +Lorem ipsum `dolor sit` amet. + +```bash + echo "Hello World" +``` +--- +dd +--- + """ + result = remove_markdown_sections( + text, + strip_codeblocks=True, + strip_frontmatter=True, + strip_inlinecode=True, + ) + assert "```bash" not in result + assert "`dolor sit`" not in result + assert "---\nkey: value" not in result + assert "`" not in result + + result = remove_markdown_sections(text) + assert "```bash" in result + assert "`dolor sit`" in result + assert "---\nkey: value" in result + assert "`" in result + + +def test_clean_dictionary(): + """Test cleaning a dictionary.""" + dictionary = {" *key* ": ["**value**", "[[value2]]", "#value3"]} + + new_dict = clean_dictionary(dictionary) + assert new_dict == {"key": ["value", "value2", "value3"]} diff --git a/tests/vault_test.py b/tests/vault_test.py new file mode 100644 index 0000000..17398ce --- /dev/null +++ b/tests/vault_test.py @@ -0,0 +1,248 @@ +# type: ignore +"""Tests for the Vault module.""" + +from pathlib import Path + +from obsidian_metadata._config import Config +from obsidian_metadata.models import Vault +from tests.helpers import Regex + + +def test_vault_creation(test_vault): + """Test creating a Vault object.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + assert vault.vault_path == vault_path + assert vault.backup_path == Path(f"{vault_path}.bak") + assert vault.new_vault_path == Path(f"{vault_path}.new") + assert vault.dry_run is False + assert str(vault.exclude_paths[0]) == Regex(r".*\.git") + assert vault.num_notes() == 2 + + assert vault.metadata.dict == { + "Inline Tags": [ + "inline_tag_bottom1", + "inline_tag_bottom2", + "inline_tag_top1", + "inline_tag_top2", + "intext_tag1", + "intext_tag2", + "shared_tag", + ], + "bottom_key1": ["bottom_key1_value"], + "bottom_key2": ["bottom_key2_value"], + "date_created": ["2022-12-22"], + "emoji_📅_key": ["emoji_📅_key_value"], + "frontmatter_Key1": ["author name"], + "frontmatter_Key2": ["article", "note"], + "intext_key": ["intext_value"], + "shared_key1": ["shared_key1_value"], + "shared_key2": ["shared_key2_value1", "shared_key2_value2"], + "tags": [ + "frontmatter_tag1", + "frontmatter_tag2", + "shared_tag", + "📅/frontmatter_tag3", + ], + "top_key1": ["top_key1_value"], + "top_key2": ["top_key2_value"], + "top_key3": ["top_key3_value_as_link"], + } + + +def test_get_filtered_notes(sample_vault) -> None: + """Test filtering notes.""" + vault_path = sample_vault + config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config, path_filter="front") + + assert vault.num_notes() == 4 + + vault_path = sample_vault + config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path) + vault2 = Vault(config=config, path_filter="mixed") + + assert vault2.num_notes() == 1 + + +def test_backup(test_vault, capsys): + """Test backing up the vault.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config, dry_run=False) + + vault.backup() + + captured = capsys.readouterr() + assert Path(f"{vault_path}.bak").exists() is True + assert captured.out == Regex(r"SUCCESS +| backed up to") + + vault.info() + + captured = capsys.readouterr() + assert captured.out == Regex(r"Backup path +\│[\s ]+/[\d\w]+") + + +def test_backup_dryrun(test_vault, capsys): + """Test backing up the vault.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config, dry_run=True) + + print(f"vault.dry_run: {vault.dry_run}") + vault.backup() + + captured = capsys.readouterr() + assert vault.backup_path.exists() is False + assert captured.out == Regex(r"DRYRUN +| Backup up vault to") + + +def test_delete_backup(test_vault, capsys): + """Test deleting the vault backup.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config, dry_run=False) + + vault.backup() + vault.delete_backup() + + captured = capsys.readouterr() + assert captured.out == Regex(r"Backup deleted") + assert vault.backup_path.exists() is False + + vault.info() + + captured = capsys.readouterr() + assert captured.out == Regex(r"Backup +\│ None") + + +def test_delete_backup_dryrun(test_vault, capsys): + """Test deleting the vault backup.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config, dry_run=True) + + Path.mkdir(vault.backup_path) + vault.delete_backup() + + captured = capsys.readouterr() + assert captured.out == Regex(r"DRYRUN +| Delete backup") + assert vault.backup_path.exists() is True + + +def test_info(test_vault, capsys): + """Test printing vault information.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + vault.info() + + captured = capsys.readouterr() + assert captured.out == Regex(r"Vault +\│ /[\d\w]+") + assert captured.out == Regex(r"Notes being edited +\│ \d+") + assert captured.out == Regex(r"Backup +\│ None") + + +def test_contains_inline_tag(test_vault) -> None: + """Test if the vault contains an inline tag.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + assert vault.contains_inline_tag("tag") is False + assert vault.contains_inline_tag("intext_tag2") is True + + +def test_contains_metadata(test_vault) -> None: + """Test if the vault contains a metadata key.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + assert vault.contains_metadata("key") is False + assert vault.contains_metadata("top_key1") is True + assert vault.contains_metadata("top_key1", "no_value") is False + assert vault.contains_metadata("top_key1", "top_key1_value") is True + + +def test_delete_inline_tag(test_vault) -> None: + """Test deleting an inline tag.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + assert vault.delete_inline_tag("no tag") is False + assert vault.delete_inline_tag("intext_tag2") is True + assert vault.metadata.dict["Inline Tags"] == [ + "inline_tag_bottom1", + "inline_tag_bottom2", + "inline_tag_top1", + "inline_tag_top2", + "intext_tag1", + "shared_tag", + ] + + +def test_delete_metadata(test_vault) -> None: + """Test deleting a metadata key/value.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + assert vault.delete_metadata("no key") == 0 + assert vault.delete_metadata("top_key1", "no_value") == 0 + + assert vault.delete_metadata("top_key1", "top_key1_value") == 1 + assert vault.metadata.dict["top_key1"] == [] + + assert vault.delete_metadata("top_key2") == 1 + assert "top_key2" not in vault.metadata.dict + + +def test_rename_inline_tag(test_vault) -> None: + """Test renaming an inline tag.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + assert vault.rename_inline_tag("no tag", "new_tag") is False + assert vault.rename_inline_tag("intext_tag2", "new_tag") is True + assert vault.metadata.dict["Inline Tags"] == [ + "inline_tag_bottom1", + "inline_tag_bottom2", + "inline_tag_top1", + "inline_tag_top2", + "intext_tag1", + "new_tag", + "shared_tag", + ] + + +def test_rename_metadata(test_vault) -> None: + """Test renaming a metadata key/value.""" + vault_path = test_vault + config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path) + vault = Vault(config=config) + + assert vault.rename_metadata("no key", "new_key") is False + assert vault.rename_metadata("tags", "nonexistent_value", "new_vaule") is False + + assert vault.rename_metadata("tags", "frontmatter_tag1", "new_vaule") is True + assert vault.metadata.dict["tags"] == [ + "frontmatter_tag2", + "new_vaule", + "shared_tag", + "📅/frontmatter_tag3", + ] + + assert vault.rename_metadata("tags", "new_key") is True + assert "tags" not in vault.metadata.dict + assert vault.metadata.dict["new_key"] == [ + "frontmatter_tag2", + "new_vaule", + "shared_tag", + "📅/frontmatter_tag3", + ]