commit 889e79174bc69862e24dd968110926858ee4b826 Author: András Schmelczer Date: Sat Apr 2 13:47:33 2022 +0200 Add files Signed-off-by: András Schmelczer diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f9c2f5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +.DS_Store +__pycache__ +.cache diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e65cd97 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["pydantic", "pyplot", "sklearn", "Tfidf", "Vectorizer"] +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..71c9115 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,2 @@ +data +.ipynb_checkpoints diff --git a/example/config.py b/example/config.py new file mode 100644 index 0000000..5276f11 --- /dev/null +++ b/example/config.py @@ -0,0 +1 @@ +model_key = \ No newline at end of file diff --git a/example/helper/__init__.py b/example/helper/__init__.py new file mode 100644 index 0000000..a465ee7 --- /dev/null +++ b/example/helper/__init__.py @@ -0,0 +1 @@ +from .preprocess import preprocess diff --git a/example/helper/preprocess.py b/example/helper/preprocess.py new file mode 100644 index 0000000..8d9e70f --- /dev/null +++ b/example/helper/preprocess.py @@ -0,0 +1,12 @@ +from sus.clean import clean +from sus.lemmatize_text import lemmatize_text +import re + + +def preprocess(text: str) -> str: + cleaned = clean(text, convert_to_ascii=True) + lemmas = [ + re.sub(r'\d+', 'NUM', lemma) + for lemma in lemmatize_text(cleaned) + ] + return " ".join(lemmas) diff --git a/example/models/__init__.py b/example/models/__init__.py new file mode 100644 index 0000000..2d8e188 --- /dev/null +++ b/example/models/__init__.py @@ -0,0 +1 @@ +from .domain_prediction import DomainPrediction diff --git a/example/models/domain_prediction.py b/example/models/domain_prediction.py new file mode 100644 index 0000000..4dce180 --- /dev/null +++ b/example/models/domain_prediction.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel +from typing import List + + +class DomainPrediction(BaseModel): + domain: str + probability: float + explanation: List[str] \ No newline at end of file diff --git a/example/predict.py b/example/predict.py new file mode 100644 index 0000000..e6c3fc8 --- /dev/null +++ b/example/predict.py @@ -0,0 +1,56 @@ + +from models import DomainPrediction +from typing import Iterable, List, Dict +from sklearn.pipeline import Pipeline +from helper import preprocess +import re +# from sus.use_model import use_model +from config import model_key + + +# @use_model(model_key, version="latest") +def predict(text: str, model: Pipeline, cut_off_probability: float=0.2) -> List[DomainPrediction]: + assert 0 <= cut_off_probability <= 1 + + feature_names = model.named_steps['vectorizer'].get_feature_names_out() + + token_mapping = { + preprocess(original): original + for original in re.sub(r'[^a-zA-Z0-9]', ' ', text).split(' ') + } + + features = model.named_steps['vectorizer'].transform([text]) + prediction = model.named_steps['classifier'].predict_proba(features)[0] + best_classes = sorted(enumerate(prediction), key=lambda v: v[1], reverse=True) + + results: List[DomainPrediction] = [] + for class_index, probability in best_classes: + weights = model.named_steps['classifier'].feature_log_prob_[class_index] + + results.append(DomainPrediction( + domain=model.named_steps['classifier'].classes_[class_index], + probability=round(probability * 100), + explanation=_get_explanation( + feature_names=feature_names, + features=features.A[0], + weights=weights, + token_mapping=token_mapping + ) + )) + + if sum(r.probability for r in results) >= cut_off_probability: + break + + return results + + +def _get_explanation(feature_names: Iterable[str], features: Iterable[float], weights: Iterable[float], token_mapping: Dict[str, str]) -> List[str]: + influential = [ + (value * weight, name) + for name, value, weight in zip(feature_names, features, weights) + if value + ] + + most_influential = sorted(influential, reverse=True)[:5] + + return [token_mapping[v[1]] for v in most_influential] diff --git a/example/secrets-backblaze.ini b/example/secrets-backblaze.ini new file mode 100644 index 0000000..8f61aad --- /dev/null +++ b/example/secrets-backblaze.ini @@ -0,0 +1,6 @@ +[DEFAULT] +aws_region_name = us-west-4 +aws_access_key_id = 00411a2454be9f90000000001 +aws_secret_access_key = K004YOSYHePVUHymVzm/vb+AjGahXl8 +large_files_bucket_name = sa-large-files +endpoint_url = https://s3.us-west-004.backblazeb2.com diff --git a/example/secrets.ini b/example/secrets.ini new file mode 100644 index 0000000..4ba33bd --- /dev/null +++ b/example/secrets.ini @@ -0,0 +1,5 @@ +[DEFAULT] +aws_region_name = eu-west-2 +aws_access_key_id = AKIAR7XRQBSIKKSKQA6U +aws_secret_access_key = WPr6mqbtJjmcmibdz12nK6XwQQHsWiXvep7NUexJ +large_files_bucket_name = sis-large-files diff --git a/example/train.ipynb b/example/train.ipynb new file mode 100644 index 0000000..0a8c3e1 --- /dev/null +++ b/example/train.ipynb @@ -0,0 +1,367 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Train Domain classifier from the [semantic scholar dataset](https://api.semanticscholar.org/corpus)\n", + "\n", + "## Download dataset (or a part of it)\n", + "\n", + "```sh\n", + "wget https://s3-us-west-2.amazonaws.com/ai2-s2-research-public/open-corpus/2022-02-01/manifest.txt\n", + "wget -B https://s3-us-west-2.amazonaws.com/ai2-s2-research-public/open-corpus/2022-02-01/ -i manifest.txt\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import matplotlib.pyplot as plt\n", + "from sklearn import metrics\n", + "from pathlib import Path\n", + "from sklearn.naive_bayes import ComplementNB\n", + "from sklearn.model_selection import train_test_split\n", + "from pathlib import Path\n", + "import pandas as pd\n", + "from joblib import dump\n", + "from sklearn.pipeline import Pipeline\n", + "from sklearn.feature_extraction.text import TfidfVectorizer\n", + "from sklearn import metrics\n", + "from random import shuffle, seed\n", + "from sklearn.model_selection import GridSearchCV\n", + "from pprint import pprint\n", + "import re\n", + "from sus.parallel_map import parallel_map\n", + "from config import model_key\n", + "from sus.clean import clean\n", + "from sus.language import is_english, predict_language\n", + "from sus.lemmatize_text import lemmatize_text" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "SS_CORPUS_PATH = Path(\"./data\")\n", + "PREFIX = \"domain-\"\n", + "SEED = 42\n", + "HYPERPARAMETER_OPTIMISATION_SIZE = 1000000\n", + "MAX_FILE_COUNT = 5\n", + "\n", + "# LargeFile.configure_credentials_from_file(\"../../../secrets/s3.ini\")\n", + "plt.rcParams[\"figure.figsize\"] = (30, 15)\n", + "plt.rcParams[\"figure.facecolor\"] = \"white\"\n", + "plt.rcParams[\"font.size\"] = 12\n", + "plt.rcParams[\"axes.xmargin\"] = 0\n", + "seed(SEED)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Preprocessing" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def preprocess(text: str) -> str:\n", + " cleaned = clean(text, convert_to_ascii=True)\n", + " lemmas = [\n", + " re.sub(r'\\d+', 'NUM', lemma)\n", + " for lemma in lemmatize_text(cleaned)\n", + " ]\n", + " return \" \".join(lemmas)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 5/5 [00:00<00:00, 56679.78it/s]\n" + ] + } + ], + "source": [ + "def clean_file(p: Path):\n", + " try:\n", + " processed_path = p.with_name(f\"{PREFIX}{p.stem}{p.suffix}\")\n", + "\n", + " if processed_path.exists():\n", + " return\n", + "\n", + " with open(p) as f:\n", + " content = json.load(f)\n", + "\n", + " result = {\n", + " preprocess(f'{c[\"title\"]} {c[\"abstract\"]}'): c[\"domain\"]\n", + " for c in content\n", + " if (\n", + " c[\"domain\"]\n", + " and c[\"abstract\"]\n", + " and is_english(predict_language(c[\"abstract\"]))\n", + " )\n", + " }\n", + "\n", + " with open(processed_path, \"w\") as f:\n", + " json.dump(result, f)\n", + " except Exception as e:\n", + " print(f\"Error ({e}) processing {p}\")\n", + "\n", + "\n", + "parallel_map(\n", + " clean_file,\n", + " list(SS_CORPUS_PATH.glob(\"s2-corpus-*.json\"))[:MAX_FILE_COUNT],\n", + " chunk_size=1,\n", + ")\n", + "None" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Found 5 files\n", + "Found 77176 documents\n" + ] + } + ], + "source": [ + "corpora = list(SS_CORPUS_PATH.glob(f\"{PREFIX}*.json\"))\n", + "shuffle(corpora)\n", + "print(f'Found {len(corpora)} files')\n", + "\n", + "data = []\n", + "for p in corpora[:MAX_FILE_COUNT]:\n", + " with open(p) as f:\n", + " data.extend(json.load(f).items())\n", + "\n", + "print(f'Found {len(data)} documents')" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "X_train, X_test, y_train, y_test = train_test_split(\n", + " [d[0] for d in data],\n", + " [d[1] for d in data],\n", + " test_size=0.1, \n", + " random_state=SEED\n", + ")\n", + "\n", + "X_train = [\n", + " x\n", + " for x, y in zip(X_train, y_train)\n", + " for domain in y\n", + "]\n", + "\n", + "y_train = [\n", + " domain\n", + " for x, y in zip(X_train, y_train)\n", + " for domain in y\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Naive Bayes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "classifier = GridSearchCV(\n", + " Pipeline(steps=[\n", + " ('vectorizer', TfidfVectorizer()),\n", + " ('classifier', ComplementNB())\n", + " ]),\n", + " {\n", + " \"vectorizer__max_df\": [0.05, 0.1, 0.3],\n", + " \"vectorizer__min_df\": [5, 10, 30, 100],\n", + " \"vectorizer__sublinear_tf\": [True, False],\n", + " \"classifier__alpha\": [0.001, 0.1, 0.5, 1],\n", + " \"classifier__fit_prior\": [True, False]\n", + " },\n", + " scoring=\"f1_macro\",\n", + " cv=3,\n", + " n_jobs=12,\n", + " verbose=7,\n", + ")\n", + "classifier.fit(X_train[:HYPERPARAMETER_OPTIMISATION_SIZE], y_train[:HYPERPARAMETER_OPTIMISATION_SIZE])\n", + "\n", + "results = pd.DataFrame(classifier.cv_results_)\n", + "results.sort_values(\"rank_test_score\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Pipeline(steps=[('vectorizer', TfidfVectorizer(max_df=0.05, min_df=10)),\n", + " ('classifier', ComplementNB(alpha=0.5, fit_prior=False))])" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "classifier = Pipeline(steps=[\n", + " ('vectorizer', TfidfVectorizer(min_df=10, max_df=0.05)),\n", + " ('classifier', ComplementNB(alpha=0.5, fit_prior=False))\n", + "])\n", + "\n", + "classifier.fit(X_train, y_train)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " precision recall f1-score support\n", + "\n", + " Art 0.41 0.43 0.42 67\n", + " Biology 0.76 0.73 0.75 579\n", + " Business 0.55 0.65 0.60 195\n", + " Chemistry 0.77 0.71 0.74 674\n", + " Computer Science 0.64 0.85 0.73 804\n", + " Economics 0.69 0.53 0.60 173\n", + " Engineering 0.62 0.31 0.42 537\n", + "Environmental Science 0.59 0.31 0.41 156\n", + " Geography 0.61 0.30 0.40 152\n", + " Geology 0.66 0.64 0.65 134\n", + " History 0.36 0.15 0.21 62\n", + " Materials Science 0.63 0.85 0.72 615\n", + " Mathematics 0.86 0.67 0.75 307\n", + " Medicine 0.88 0.92 0.90 2138\n", + " Philosophy 0.60 0.29 0.39 52\n", + " Physics 0.75 0.62 0.68 351\n", + " Political Science 0.49 0.55 0.52 214\n", + " Psychology 0.54 0.69 0.60 324\n", + " Sociology 0.36 0.35 0.36 184\n", + "\n", + " accuracy 0.72 7718\n", + " macro avg 0.62 0.56 0.57 7718\n", + " weighted avg 0.72 0.72 0.71 7718\n", + "\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABHYAAAPfCAYAAACihU4oAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAAsTAAALEwEAmpwYAAEAAElEQVR4nOzdd3xTVR8G8CdN0zaje9JBacsQKsiW5QLKUhDKRkSG4gBexfUqOBAZigP1xQGylyiylSEbWQpoWYW20N3S0kFXRtuM949A05CUBkxsLzzfz6cfpT1Nnp7cnHPzu+feKzIYDAYQEREREREREZHgONV1ACIiIiIiIiIiujMs7BARERERERERCRQLO0REREREREREAsXCDhERERERERGRQLGwQ0REREREREQkUCzsEBEREREREREJlHNdByCqjVguh8Tbp65j2MQlS1nXEYiIbCOq6wC3wVDXAW6DQlrXCWxXpq7rBFTHRGJxXUewmUGnq+sIdyeRcCYDkbMwPrqqdSWo0HF8tZfej8lRUFi/3/9+DR7Fzp076zSDMN4ddE+TePsgdPLUuo5hk4hpx+o6gu0ENJHDIKBPdexXEgih7CADgEEvnG1V36ZlXUewmdPhuLqOQHVM7OFZ1xFspisqrusIthPQvoDIWVLXEWwmDvSv6wg2OZrzQ11HuKsUFOrw566GdR3jljo+kV/XEXgqFhERERERERGRUAnncB0RERERERER3TMMAPTQ13WMeo8rdoiIiIiIiIiIBIqFHSIiIiIiIiIigeKpWERERERERERUDxmgM/BUrNpwxQ4RERERERERkUCxsENEREREREREJFA8FYuIiIiIiIiI6h3jXbEMdR2j3uOKHSIiIiIiIiIigWJhh4iIiIiIiIhIoHgqFhERERERERHVS3rwrli14YodIiIiIiIiIiKBYmGHiIiIiIiIiEigWNghIiIiIiIiIhIoFnboruHposHXj+1E3FOLsX/IajwRkXTL9hInHXYMXIdDQ1eZff/Dzgexc9APuPjMdxjU+KJDsrp7afHekhRsuXQWK/+Mx2ODrtXQ0oAJ07Ox/tw5rD93DhOmZwPVbvcXGa3Ggp2J2HL5DBbsTERktNoxWRenYEvSGaz84zweG3iLrNOysf7cWaw/dxYTpt2cVYUFOxKw5dJpLNiRgMholWOy3mP9+vLHGVh86AJ2ZMQhZliB3XNWZRVSvwogq1ByAoDCU4t3F13G5ot/Y8XRs3j0ycIas45/OxM/nY7DT6fjMP7tTLOs//koDYv3n8P21FOIGZJv95zAjffVZWxJjMPK4+fw2MCas06YloX1Z09j/dnTmDAtC+bvqzQsPngeO9L/QsxQB72v5OV4//V92LpyDVZ//TMe65pstd0D0VfwyXu7sHn5Wqxa8LPVNoP6xmPlgg3YunINlny+GSENiu2bVUDbK7M6ahyoxDtfncfGk4exfM8fePTxqzVmHfdqMtYdPYp1R49i3KvJZllv6D4gF9vjD6H34Ct2zyqkfhXSPpbCU4t3FyZh84VTWHHkNB59sqax0YDxb2Xgp7i/8FPcXxj/VkZV1pAIDd7/Pgnr/vob60//hdkrExAaad9+VXhUYPq8U9hwcBeWbdmHR3pn1Zhz3OSL+GH3bvywezfGTb5YlTO6dSF+PrDL7OvXP7ejy2P2316pdgYYoDPU76/6gIUdumu83+kwKvVidPnxGbx+qAc+6Pw7GnvVtFMPTLg/DoUaqcX3L17zxQfHH8L5An+HZZ00JwvaShGGt2qBjyc3xJS5mQhvqrFo1290ITr3KcGLMU3xQs9meDCmBI8/bZxInSV6zFiWgr0bvDGk+f3Yvd4bM5alwFli34uLTZqdacz6QDQ+nhyOKXMzEN7UchLuN7oAnfsU48WYZtezFptnXZqCvRu9MaRFS+xe74MZSx2Q9R7rVwBIjnfDgmmhuHTWclu2W1Yh9atAsgolJwBMnpUObaUII9q2wryXIzBldpr1bfWpfHTpVYSXerfAi71a4MGexeg32lTASYmXYsE7DXHpnMyu+aqbNCsD2goRhrduiY+nNMKUOek1Zu3cuwgv9mqOF2Ka48GexXi8WtbkeBkWTAvDpbOOyzrl2ePQasUY9twwzP3qIbz83HGEh1p+qNNonLFrf2MsWtXe6uP07Z6IPt0v4Z25PTBgzCi883F3lJS42TWrkLZXZnVM1pfeuQRtpRNGPdwZ8968D5PeS0LDxkqLdn2HXUHnHgWYNKgdJg1shwcfK0S/4eYfhhUelRg+MR2pSY55fwmpX4W0jzX5wzTjXNCuNea9HIkps9IQ3sRK1lF5xrmgz/14sff9eLBnEfo9lQcAkHtocXyPF559rCVGtGuNhNNyvP/9JbvmfOmN89BWOuGpPj3wyXutMem/59EwstSiXZ9BGej0SC4mP9UNk0c9hI7drqJvbDoA4HycD4Y82rvq64NX20OlFOPUMcd9NiD6p1jYIbt59NFH4e3tjfLy8lu2Gzt2LN555x27PrfUuRK9wpPxxd8doNJKcOpqA+zLCMfAqESr7UMVJXgyMgkLz7ax+Nmai/fj2JVQVOjEds14g6tUh279irFiXgNoVGKc/1OBY795oscQyyJUzLBCbPjOH/lXXFCQI8GGhf6IGWbc8W/VRQmx2IBN3/uhssIJW5b4QyQCWncts3/WT4KMWU8ocGy3J3oMtvzwETO0EBsW3sjqgg0LAxAzzPg3tepcBrEY2PS9vzHrUgdmvYf6FQC2rfBH3GF3VJQ7ZjgXZL/W86xCyXkja9e+RVj5aXDVtnp8jxe6x1oeqe05uAAbvg9Efo4LCnJdsHFRIGKGmNptWxmAuCMeqCwX2S3fzVm79SvCik+Cq72vvNBjsJV+HVqIDYsCTe+rRQFmK962rfBH3BEPVDgoq5trJbo9mI7lP7aGplyC8wmBOHYyDD0ftly1k3DZH3t+j8KVqwqLn4lEBowechrfreiA9CwvACJcyfVAqdLVblmFtr0yq4PGgV75WPVVODQqMeL/8sQf+33Rvb/lqp0eT17FxuWhKMh1RcFVV2xcFoKeA3PN2oydmoqtq0NQck1it4zVswqpX4W0j9W17zWs/CzUmPWk+/W5wHL1Zc8h+eZzwfdBVas0E08rsOtHf5QVO0OndcLGxUEIa6yBu5fWPjndtOjSPQerFjaBRu2M+NM++ONQALr3tVy10/PxTGxaE4GCq1IU5Llh09oI9Hzc+uqeHo9n4si+IJRreENpqr9Y2CG7SE1Nxe+//w6RSIStW7fW2E6n0znk+Rt5FENncEJqiVfV9y4U+qKxl/Ulre8+eBif//UgNA4q3txKaFQ5dDogK9m0450S74bwZpYFsfCmGiTHm468Jp+XIryZpupnKRekAEwfPIyPY3lU6o6zRt7IasqQUi2DZVbTqpHkeGnVEbLwZhqkXHAzz3rB+uPccdZ7sF//DYLqV4FkFUpOoNq2mlItQw3bYHhTNZLjTUfgky9IrR55dhRrWVPirWcwZq2791VIgxLodCJkXfGs+t7lNG+EhxXd1uP4+SgR4KdCo7BrWPPNeqxcsAFjhsZBJLLfsnBBba/M6pCsIY3U0GlFyEqr9v5OkCPcyoqd8MZKpCTITVkSFGjY2HRaUNOWJWgSXYrtPzawW77qhNSvgtrHitQYx6zqc0ENY3x4Ew2SL1TbVuJlNc4FLR8sReFVCUqL7FMwCWmohE4nQna6qRCekuSBhpGWRa6GkWVISfIw5Uxyt7qyx9VNi67dc7D311C7ZKQ7o4ehXn/VByzskF2sXLkSnTp1wtixY7FixYqq748dOxYvvvgi+vXrB7lcjiVLlmDNmjWYN28eFAoF+vfvb5fnlztXoqzS/MhPWYUL5JIKi7YxDVMgFhmwOz3CLs99u6QyPVSl5gUlZYkYUrll0ctNbt5WWSqGTKEHYIBUrofy5scpFUOqsN/SW6ncStbSW2QtuY2sNfzNd5z1HuzXf4Og+lUgWYWS0/j8OqvbqqyGrNXzKEv+5W21pveVlf6o8/eVmxYqtfmcpVS5QOZWeVuP4+9r/MDc7oFsTHz9SbzxQW881jUFfbrf+hpzt5VVQNsrszoqqw4q5c3P4Ww9q0wHZanpQ7qyzPn6eGGAk5MBk969hG9mNYbB4JjVcILqVwHtY7nJ9FCVmn9sVJY4Qya3Nr7qzOeCGsZXv6AKTPowDYs+DLNbTqlMB7XSvEikLHOGVGa5IshNqoWyzNRWVSap2lar6/JYLkqKXXD2Lx+75SRyBK4nI7tYuXIlXn31VTz44IPo1KkTcnNzERgYCABYu3Yttm/fjl9++QUVFRU4evQoQkNDMWvWrBofb9GiRVi0aBEAQKe0PCJ0M6VWAoXEfIdY4VIJZaWL2fekzpV4o/0xPLe73+3+iXajVjlB5m4+2crcdVDftNMEABql0/XJ8Ho7hQ6qMicAIqiVTpApbn4cPdRl9qvXqpW3mbVa29qzWn+cO856D/brv0FQ/SqQrELJaXx+sWVWhd7iQ54pa7Vt1f1f3latva8UOqv9Yfm+0v+7WTXOkEnN5yy5tBIqze2dmlJeYXwdftpyP5QqFyhVLvh1T1N0bJOJHXub2iergLZXZnVUVstirkxRQ1aVGDKF6UO0TK69Pl6I8PjILKQkypFwxsPi9+yXVUD9KqB9LI3KCTJ38yKOzF0HldLa+Co2nwus7Ld4+lRi9uoE/LIqAAe2+totp1olhlRuXsSRybVQqyw/8mrUzpDJrW+r1fV8PBP7fg2x+D5RfcMVO/SPHT58GGlpaRg2bBjatWuHqKgorF27turnTz75JLp27QonJye4udl2QceJEyfi5MmTOHnyJMRyea3tU0s8IRbpEe5eVPW9+7wLcKnI26xdI49ihCjKsLbfFhwZvgILHvsN/lIVjgxfgRBFiW1/8D+UedkVYjEQHGFaFhzZQoO0BMtrIqQluiGyhWn5amS0GmkJblU/i2iuQfUjCxHNTT+3S9Zka1mtP4cxq8a8XeL1rAluiGjh4Kz3YL/+GwTVrwLJKpScQLVttVH1bVBldRtMS5QisoXplIvI5mqkJTruot43M72vbn6/WGYwZq3WrzX8TY6SdcUDYrEBIUGmeScyvBBpGV639TiZ2Z6oqHQyO75s75tzCGp7ZVaHZM1KlULsbEBweLUMzcqQdsly/yztkhwRzUwH5CLuUyL9kvG0nNaditC5RwFWHzqG1YeOoXmbEjz7ZjJenG6/i+cKqV8FtY+V7Aax2GA+FzRXWR9fk9wQ2fzm8dXUTuGhxezViTi+2wvrFgTbLSMAZKXLjTnDqm2DTUuRnmx5jbL0ZAUimpjG4IgmJUhPdjdr4xegRsu2hdi7PcSuOen2GADoYKjXX/UBCzv0j61YsQK9evWCn58fAGDUqFFmp2OFhdlviWVN1FoJdqdH4OU2JyF1rkTbgCvo0TAVmy+bH7FMvOaDR9aPxpNbh+LJrUPxzpFHkK+R4smtQ3FFaRz0JU46uIi1AAyQiPRwEWshsuMbtlwtxpEdnhjzRg5cpTq06KBE597F2Puz5RLPPeu9Eft8HnyDKuETWIkhz+dh90/GYtWZo3Lo9cDACfmQuOgxYJzxwnRxRywnr3+c9fUrxqzty9C5VzH2bvC2aLvnZx/ETrwK36CKalmNf9OZYwroddWyjs1zXNZ7qF8B490wJK56iESAszOu//89vr3W86xCyVmVdacXxryWbdpWY4qwb6PlEdY9G30R++xV+AZWwCewAoMn5mL3z6Z2N7ZViACxxOCgbdULY16r/r4qwt4NVvr1Zx/EPpd7/X1VgSETr2L3T5ZZRSLA2QFZNeUSHP6jIZ4Z/jfcXCsR3ewqunTIwJ5DkRZtRSIDJBIdnMXGPMb/Nx4NL69wxsGjjTBswHlI3Srh56NEv56J+OOU/eZdwW2vzOqQrEd3+2H05FRj1jbF6NS9APu2BVi03bc1AIOeyYRvQDl8/MsROzYTezYbV3B/Pq0ZXujfHlNi22FKbDsknXPH2m/CseLLRnbNKqR+FdQ+1k5vjHk163rW0utzgZ9l1g1+iH0uxzgXBFRg8HM52P2zsZ1MocPsVYmIP6nAso/t//mgXOOMo/uDMHpiIlzdtGjeqhCdHs7Fvh2WhZm920MwaFQKfP018PHTYNBTKdjzq3m77v2ycOGsF3Kyaj/ITFTXRAZDPbnxOgmSWq1GUFAQdDodFArjBFJeXo6ioiLExcVh/vz5FqddjRs3DiEhIbc8Fas6t9AwhE6eWms7TxcN5nY7gC4NMlFU7oZPTz2IX1KaoH3AFXwf8yvarHnW4nc6BmXh04f24eH1T1d9b1WfLXgwyPzWnKN39sefObVX6yOmHbPhLwLcvbR49fMMtH24DCXXxFg6pwH2b/LG/R3LMGtNCgY2aXm9pQET3rmCviONdz7Y8YMPlsxqgBvLQaPuV2Hqp5lo2ESD9EtumP9aKC7beithkW1LSt29tHj1s/RqWYOxf/P1rKuTMbBpK1PW6VfQd2TB9ay+WDK7WtZoFaZ+mlEtaxgun7cxq43D1L3Yr/PWJ+GBLuanK74xJApnjpkfdbJKSP1qI6FkrQ85Rc62nY2t8NTi1U9T0fahUmPWj0JxYIsPojuWYtaKSxjU/MbdBQ2YMC0LfUYYPwDtXOeHJXNMy9fn/ZiAVp3NL2D55rCmOHO89m3VoL+NbfXTNLR9+HrWuSHYv9nH2K+rLmFgs9amrNOzbnpfVcu6PhEP3JT1jaFNbHpf6bu0rLUNALjLy/HaS0fQtuUVlJa5YvGatth/JBL335eLOdP2YMCYpwAArVrk4LMZu8x+9/T5QLz+QR8AgExagVcmHsODbTNRpnTBjr1NsXpDK9hy2oDT4TjbstaD7dVWzHp7WcVenrU3AqDwrMTUWYlo0/kaSoolWP55BA78GoDodsWYufAsBrfvVpV1/Gsp6D0kBwCw6+cgLP0sAta2x4+Wn8b+bQHYtcG2Cynrioptalcf+lVI+1giZ9tOAVV4avHqJylo+1AJSq45Y+nHoTiwxRfRHUoxa0UiBrVoZ8r6dib6jDAWmHau88eSuaEAROg5OB+vf54CjcrJbBdkYs/7kZdd+938xIG1325c4VGBV949izYd843b6tfNcHBXCKJbF+KDL05gyKO9q3KOm5KA3gMyAAC7toZh2f+aofq2+t1PB7FxdSR+23p7RaijOT+guCK39oZkk9YPuGDvDstCcn3Se0AQTp48WacZWNihf+SHH37ApEmTEBcXBxcX0/Vshg0bhg4dOqCgoMCisPPWW28hPT3d7HStW7G1sFMf2FrYqRds3OmoF4Q0TLFfSSBsLezUB7YWduoDWws79YGthR26e9la2KkPbC3s1AsC2hewtbBTH9hS2KkPWNixr9YPuGD3jvr92vcd0KDOCzs8FYv+kRUrVmDcuHFo2LAhgoKCqr4mT56MNWvWQKu1vAr9hAkTEB8fDy8vLwwcOPDfD01ERERERER0lxDO4Tqql3bu3Gn1+8OGDcOwYcOs/qxJkyaIi4tzYCoiIiIiIiKiewMLO0RERERERERU7xgA6Hj5gFrxVCwiIiIiIiIiIoFiYYeIiIiIiIiISKBY2CEiIiIiIiIiEiheY4eIiIiIiIiI6iV9XQcQAK7YISIiIiIiIiISKBZ2iIiIiIiIiIgEiqdiEREREREREVG9Y4ABOvB257Xhih0iIiIiIiIiIoFiYYeIiIiIiIiISKB4KhYRERERERER1T8GQMczsWrFFTtERERERERERALFwg4RERERERERkUDxVCwiIiIiIiIiqncMAPR1HUIAWNihes8lW4XI90/VdQybPJ2QUdcRbLayeaO6jmAzsY9nXUewme5acV1HsJ2orgPcBr2urhPYxMnNra4j2Myg1dZ1BJuJfX3qOoLtDsfVdQKqY2Jv77qOYDsDPy7d6wzayrqOYDNdTm5dR7CJkPqU7h48FYuIiIiIiIiISKC4YoeIiIiIiIiI6iERdIJaZl43uGKHiIiIiIiIiEigWNghIiIiIiIiIhIonopFRERERERERPWOAYDeUNcp6j+u2CEiIiIiIiIiEigWdoiIiIiIiIiIBIqFHSIiIiIiIiIigeI1doiIiIiIiIioXuLtzmvHFTtERERERERERALFwg4RERERERERkUDxVCwiIiIiIiIiqncM4KlYtuCKHSIiIiIiIiIigWJhh4iIiIiIiIhIoHgqFhERERERERHVS3oDT8WqDVfsEBEREREREREJFAs7dNdQeGrx7sIkbL5wCiuOnMajTxbU0NKA8W9l4Ke4v/BT3F8Y/1YGjJflMvrP3FQs3ncW21NOIGZIvkOylhc5Yf8kX6xtHYINjzVA8jaZ1XZ7nvXD2jYhVV+r7w/F1v6BZm0urFBgY/cGWNs6BFv6BqEkxb4L8dy9tHhv8WVsSYzDyuPn8NjAwhpaGjBhWhbWnz2N9WdPY8K0LFTv15c/TsPig+exI/0vxAyt6bX5ZxQelXjni7PY+MdBLN91FI/2y60x67ipl7Hu99+x7vffMW7qZbOsN3Tvn4PtZ/ejd2y23bMKqV8Fl3VJCrZcOouVf8bjsUHXas46PRvrz53D+nPnMGF6tlnWyGg1FuxMxJbLZ7BgZyIio9V2zanw1OLdbxOx6dwJLP/9bzw6oKaxxoDx/03Hj6dO4cdTpzD+v+lVOUMi1HhvYQLWnTiFn/46iVnLLyIkwr45q7IuuozNF//GiqNn8eiTNb/+49/OxE+n4/DT6TiMfzsTZmPrR2lYvP8ctqeectjYqvCoxDvzT2Pj8X1YvuMwHu2bU2PWca8kYd3Bg1h38CDGvZJklnX76T3YeHwfNhzbjw3H9uPl9+PtnlUo2yqzOi6rwrMS73x5DhtPHMLy3cfw6OO3mLNevYx1Rw5j3ZHDGPdqDXPWgBxsP38AvQfbf85SeFbina/OY+PJw1i+5w88+vjVW2RNxrqjR7Hu6FGMezW5hqy52B5/CL0HX7F7ViFtA8b5NQVbks5g5R/n8djAW2Sdlo31585i/bmzmDDt5qwqLNiRgC2XTmPBjgRERqvu2axCmrOI7ImnYt2jXnjhBYSEhODdd9+tta1IJEJSUhIaN278LyS7c5M/TIO2UoQR7VojqoUKM5clISVehrQkqVm7fqPy0KVXEV7qcz8MBmDOmgTkZLhi+5oAAEDKBSkO/eJzveDjGH/M9IKTBBh6JBuFFyTY97w/fO6rgFcTrVm7novNJ5JdT/sj6MHyqn8nrZcj6Wc5ui/Kg2eUFmUZYrh4WO5A/ROTZmVAWyHC8NYtERWtxocrLiE5Xoq0xJv69al8dO5dhBd7NYfBAMxdewk56S74dbU/ACA5XoaDW72vT/CO8dL0RGgrnTDq0a6IvK8MH3x9BskJCqRflpu16zs0G50fy8OkIR0AgwizF8UhN9MN29eHVLVReFRi+HNpSE2S3/w0diGkfhVU1jlZ0FaKMLxVC0Tdr8aHK1OQfF6KtEQ386yjC9G5TwlejGkKg0GEuesuG7Ou8oOzRI8Zy1Kw6Xt//LLCF/2eLsCMZSkY3/U+aCvtczxk0sxUVFaKMLJjW0S1UOGDJQlIviBDepJ5kbfvyKvoHHMNkx6/HwaDCHNWXjCOV2sDIXfX4fheb3z+ZhTUSieMmpKF9xclYmLMA3bJeMPkWenGsbVtK0RFq41j6wXrr3+XXkV4qXcL49i6NsmY9frrnxIvxaFt3hj/dpZd81X30rSLxjHgsYeNY8D//kZyogLplxVm7foOyTKOAUMfBADM/u5v5GZJsX19aFWbSUM74UqG9aK7PQhlW2VWx2V96Z0kaCtFGPVIF+P2+s1ZJF+0NmddQefu+ZgU2944Zy0+bZyzfrI2Zzlmm33pnUvG99bDnY1Zvz2H5AQ50i/dlHXYFXTuUYBJg9oBBmD2krPIzXLD9h+DzbNOTHdYViFtA5NmZxqzPhBtnF9XJiM53s1yfB1dgM59ivFiTDPj/PrDZeRkVMu6NAWbFvvjlxV+6De6ADOWpmB8t+b3ZFYhzVlkG94VyzZcsXMXa9SoEaRSKRQKBby9vfH4448jI8NYrPjuu+9sKuoIhatUh659r2HlZ6HQqMQ4f9Idx/d4oXusZYW955B8bPg+EPk5LijIdcHG74PMKvHbVgYi7ogHKssd8/aoVImQ/psMbV4uhkRuQGD7CoR1V+PyllsXEMoyxbh60hVRA5UAAIMeOL3AAx2mFcGrsRYiEeDeUAdXL73dsrpKdejWrwgrPgk29usJBY7t9kKPwZZHP2KGFmLDokDkX3FBQY4LNiwKQMww06qMbSv8EXfEAxXljhmYXaU6dI3Jw6oFEdConRH/txf+OOCH7v0tj9j3GJCDjSsboiDXDQVXXbFxRRh6PmnebuzLydi6JhQlRRKHZBVSvworazFWzGtgzPqnAsd+80SPIVayDivEhu/8r2eVYMNCf8QMMx59bNVFCbHYgE3f+6GywglblvhDJAJady2zW86uvQuxar75eNVjkJXxKjYfGxc3QH6OKwpyXbBhSQPEDM4DACSeUeC3nwJQVuwMndYJm5Y2QFiUBu5elXbJWZW1bxFWfmp6/Y1jq+WKq56DC8zH1kWBiBlS7fVfGXB9bHXgGNDzKlZ9HWkaAw76o/sTlisCevS/YhwDrrqh4KobNq5qiJ4DHFdwtJZVCNsqszo2a9eYPKz6XwQ0KmfE/+WFP/b7ofsAK3PWkznYuCLMNGctD0PPgTfNWa9cn7OuOWbO6torH6u+CodGJUb8X574Y78vuve3XLXT48mr2Lg8FAW5rsasy0LQc6D5SqSxU1OxdXWIw7IKaRvo1q8YKz4Jqja/eqLHYMuVMDFDC7Fhob9pfl0YgJhhxr+pVecyiMXApu/9jVmX3rtZhTRnEdkbCzt3uW3btqGsrAxXrlxBYGAgpkyZUteRHCI0UgOdToSsFNPRmOQLUoQ3tVw2G95Eg+QLpqNEyfEyq+0cpSTVGSKxAR4RptU53vdVovjSrXdwLm+WI6B9ORShOgCAMkcMVY4zihIl+PmRBtjYvQHivvKAwX51HYRGlkOng1m/psTX0K9N1UiONx0NSY6XIrypxn5hahESroJOK0JWWrXXNkGB8CilRdvwKCVSEkyFtJQEBRo2NrVren8JmkSXYvtPwRa/aw9C6ldBZY26njXZtep7KfFuCG9WbtE2vKkGyfHVxovzUoQ301T9LOWCFKh2dMj4OPb5W0IjboxXpr5KuSBHeJMa+rTaeJVyQYaGVtoBQMuOJSi8KkGpHYuR1l7/ml5X4+tf7f1XwxjsKCHhyutjgOm9nZzgXsMYUIaURPeqf6ckuKPhTe3mLT2J1XsPYfrnpxEQbN+/QyjbKrM6Lqv1OUuO8MaWp6WEN1Yi5aJp1VlKghwNq7Vr2vL6nPWjY+askEbqGrJaeW81tja/WsvawCFZhbQNVI2vydXm12oZLLNan1/Dm2mQcsHNPOsF649zt2cV0pxFZG8s7Nwj3NzcMGTIEMTHG68TMHbsWLzzzjtVP//+++/RuHFj+Pj4YMCAAcjOtn7ksri4GGPGjIG/vz/Cw8Mxa9Ys6PXGSoJOp8Nrr70GPz8/REREYMGCBRCJRNBqtVi/fj3atWtn9liff/45nnzySfv8fTI9VKXmm7OyxBkyuWWVw02ug7JUbGpXKoZMoYe1c8AdQasSQaIwfy6Jux6VylsfEUjeIkPUINNOlCrH+DdkH3HDgG056LXyKlJ+lSHpZ/udOiSV66Gq1leAsb+kCmv9qoeqpO76VSrTQaU0P7tUWeYMqVxn0dZNpoOyzNmsnUyuA2CAk5MBk95JxDdzmsDgoCvwC6pfhZRVZiVridj6NnDT31U9q1SuNxsjbvzc2t98J9zkOqjKrDx+TduqWU5nq33qF1SOlz5IxaLZDe2S0Syrlb6Q1dCnZllL/uXXX1rDGCDTWrQ19qv1MQAA3hzXDuP6dsPzAzujMM8VM/4XByex/armQtlWmdWRWXVQKW96jlttr2U3jQM3z1mzHThnWctaeov5tYb3lpOTAZPevYRvZjV2YFYBbQM1za81Za1hfrWatYa/+W7PKqQ5i2xngAg6ONXrr/qgfqQgh1OpVPjxxx/RqVMni5/t27cPb7/9Nn766SdcuXIF4eHhGDFihNXHmTJlCoqLi5GcnIyDBw9i5cqVWLZsGQBjcWjHjh2Ii4vDX3/9hc2bN1f93oABA5CSkoILFy5UfW/VqlUYM2aM1edZtGgR2rdvj/bt26PSUHsVX6NygszdfLKVueugUlpu4hqlGDKFaYCXKXRQlTkB/9K5m84yAyrLzJ+rsswJEnnNE0nuSReo88UI7206kiB2M7aPfrYULh4GKEJ1aDpciayDbjU9zG1TK50gczefDGUKHdRl1vrVvK1Mof9X+1WtEkMmN98hlsm1UN+0MwoAGpX5JC9T3NhpFeHxEVlISZQj4Yyn47IKqV+FlFVlJau7zvo2oHS6vgN3vV21cUCtdDIbI4yPo7f6N9+Jm8egG89f47Zay3jl6VOJ2Ssu4tfVgTi4zc8uGc2yWrz+eosPeca25v0mc/93x1a12soYoNBCrbK8nKCxX01tZXJt1RgAAOf+8oZW6wRlqQQLP26GoBA1GkbY7wKfQtlWmdWRWS0/bMrkultsr7easxSOnbOsZb3lmGX9vfX4yOzr86uHA7MKaBuwNr/eKqu79bnAelbrj3O3ZxXSnEVkbyzs3OUGDhwILy8veHp6Yvfu3XjjjTcs2qxZswbjx49H27Zt4erqirlz5+LYsWNITU01a6fT6bBu3TrMnTsX7u7uaNSoEV577TWsWrUKAPDTTz/h5ZdfRmhoKLy9vfHWW29V/a6rqyuGDx+O1atXAwDOnz+P1NRUPPHEE1ZzT5w4ESdPnsTJkychEdVeqMhMdoNYbEBwI1MRKLK5yuJCaQCQluSGyOamAklkC+vtHMWjkRYGnQglqaadt2sXJfBsXPN1MS5vlqNhjNqs+OMZoYWTxABRtflHJLLvUYbMZFeIxUBwRLV+baG23q+JUkS2uLlf7Vdkqk1WmgxiZwOCG5o+fEU2K0PaZcsVTGmX5YhoZjqfO6JpWdUFIFs/eA2de+Rj9f4jWL3/CJq3Lsazr1/Ci9MS7ZZVSP0qqKyXb2Q1LbmPbKFBWoKrRdu0RDfzrNFqpCW4Vf0sorkG1Y/aRTQ3/fwf50yxHK8imqssLvRuzCJFZPNq23RzJdKrtVN4aDF7xUUc3+uNdd+EWPz+P8564/WvPrbW8LoaX//qWa1vJ46SlSa3HAOaltYwBigQ0bTaGNCs1OKCtdUZDCLAjuOrULZVZnVc1hrnrEuWFxROu3TTnNWsDOnX27XudH3OOngEqw8eQfM2JXj2jct4cbr95qysVKkxa3i1/mpWhrRLVt5bl+SIaGZaXRxxn7Ja1iJ07lGA1YeOYfWhY8asbybjxemX7JZVSNuAaX6tntX6cxiz3jwPX8+a4IaIFsxqllMAcxaRvbGwc5fbvHkzioqKoNFosGDBAjzyyCPIyTG/4F52djbCw8Or/q1QKODr64usLPOrwOfn56OystKsbXh4eFW77OxshIWFVf2s+v8DwDPPPIO1a9fCYDBg1apVGDZsGFxdLSfaO1GuFuPITm+MeTULrlIdWrQvReeYIuzbaHn0es8GP8Q+lwPfwAr4BFRg8HM52P2zqZ2zRA+Jqx4QAWJnAySuersWTCQyAxrGqBH3lQcqVSJcPeWCjL1SRD1pea46AGg1IqTtMD8NCwCcpQY06qfCucXuqCwTQZkjRuKPCoQ+ar9zqsvVYhzZ4YUxr1253q9l6NyrCHs3+Fi03fOzD2Kfy4VvUAV8AiswZOJV7P7J15T3er+KRICzxP79Wq4W4+gef4yelGLM2roInR7Lx75tQRZt920NwqAxGfANKIePfzlin0nHni3Gdp+/cx9eGNARU4a0x5Qh7ZF03h1rv2uEFV9F2jWrkPpVWFk9MeaNHGPWDkp07l2MvT9bybreG7HP58E3qBI+gZUY8nwedv/kDQA4c1QOvR4YOCEfEhc9BowzXtQ47ojC4nHuNOfRXd54emqmMWe7UnSOuYa9myzHq72b/DBowpWq8Sp2Qg52bzDesUOm0GLWios4f8ody+bZ9xSs6lmP7PTCmNeyTa9/TBH2bfS1aLtnoy9in71qzBpYgcETc7H7Z8vXHyJA7KgxYG8ARr902TQGPJqHfb9YXstj3y8NMOjpNPgGaIxjwJh07NlqvD5Jw6gyRDYrhZOTAW5SLZ59LREFV12RkWK/01yFsq0yq2OzHt3th9FTUo1Z2xSjU/d87Ntqbc4KxKAxmaY5a2wG9my+PmdNvw8vDOiAKYPbY8rg9kg654613zbCii/tO2cd3e2H0ZOrZy3Avm0BVrIGYNAz1bNmYs/mQGPWac3wQv/2mBLbDlNi2xmzfhOOFV82smtWIW0DR3Z4Yszr1efXYuzd4G2Z9WcfxE68en1+vZHV+DedOaaAXlct69i8ezarkOYsInsTGQwGbqF3qUaNGmHx4sXo2bNn1ff8/f3x7bff4pdffkFoaChmzZqFCRMmwNfXF/PmzQMAKJVKeHl5ISkpCY0aNaq63XlERASkUini4uLQokULAMZTptauXYsDBw7gsccew8iRIzFx4kQAwJ49exATE4PKyko4OxtXpzRr1gzff/89Ro8ejbVr16Jbt261/h0eTr7oJOlTazuFpxavfpKCtg+VoOSaM5Z+HIoDW3wR3aEUs1YkYlCLG9f4MWDC25noM8I4mexc548lc0NxY+nlvHUX0apzqdljvzm8Gc4cr33p8NPnLtfaBgDKi5xwdJo3rhx1g4uXHm1fK0ZkfxVyT7pg73P+GPW3qaiW8osMf33midh9V8xW5wBARZkIx9/1QeYBN7h46NFkqBKtJpVYtLNmZfNGNmV199Li1U/T0PbhUpRcE2Pp3BDs3+yD+zuWYdaqSxjYrPX1lgZMmJ6FviONdxTY8YMvlswOQVW/rk/EA53N73rwxtAmOHPMHbURe9u2xFzhUYmpH15Em06FKCmWYPkXUTiwPRDRbYsw89szGPzgw1VZx0+9jN6DjXfL2bWhAZbOj4K15bcfLf0b+38JxK6Ntl2UUnet2KZ29aFfbVUvsuptO//e3UuLVz/PQNuHy4xZ5zTA/k3exqxrUjCwSUtT1neuoO/IwutZfbBkVoOqrFH3qzD100w0bKJB+iU3zH8tFJfP1X5rXic32446Kjy1mPpxMtp2K0ZJkTOWzQvDga1+iO5Qgg+XJiC2ZYeqnOP/m4E+w413n9n5YwCWfhwGQISesXl47dNkaFROqD6TP9+7FfKyay+aG7SW1/KoKeurn6ai7UPXX/+PQnFgiw+iO5Zi1opLGNS8TVXWCdOy0GeE8QPQznV+WDKn2uv/YwJa3fT6vzmsKc4cr/31d/K2/PBgNatHJaZ+EI82nQtQUiTB8i+b4MCOIES3uYaZ38RhcOfHqrKOf+USescax9pdG0Ow9IvGAER4oGMhJk2/CL9ADTRqMS7EeWHJ/CbITrft1sy6vDyb2tX1tno7mPX2sopt3V49r89Zna8Z56z5kTjw6/U5a+EZDO5Qbc56Ldl8zvosElbnrGXX56wNNl5I2cY7Lig8KzF1VqIp6+cROPBrAKLbFWPmwrMY3P7G/pwB419LQe8hxoOIu34OwtLPIqxnXX4a+7cFYNcG2y6krCu6jfm1rrdXW3bEbmT9LL1a1mDs33w96+pkDGzaypR1+pWb5tdqWaNVmPppRrWsYbh83gHvrTrOKhLXfspWfZizjmt3oURveSc2ujPNW7li+TbHXBzeXiYN9sXJkyfrNAMLO3ex6oUdg8GArVu3YvDgwTh9+jQ++eSTqsLOnj17MHLkSOzevRvNmzfHm2++iVOnTuHw4cMAUFXYady4MUaPHg2lUomVK1eisLAQvXv3xuuvv45nn30W3377LRYsWIDffvsNcrkcQ4cOxZ49e8wKO7Nnz8aPP/4IpVKJy5dtK4LYWtipD2wt7NQHthZ26gNbCzv1ga2FHbpNNhZ26pqthZ36wNbCTn1ga2GnPrC1sEN3L1sLO/WCPW+l6WC2FnbqBRsLO3R7bCns1Acs7NgXCzu24alYd7n+/ftDoVDAw8MD06dPx4oVKxAdHW3WpmfPnvjwww8xePBgNGjQAJcvX8a6deusPt7//vc/yOVyREZGolu3bhg1ahTGjx8PAHjuuefQq1cvtGrVCm3atEG/fv3g7OwMcbVB+Omnn8a5c+cwevRox/3RRERERERERPcIrtghh9mxYwdeeOEFpKWlVX1PrVYjICAAf/31F5o0aWLT43DFjmNwxY5jcMWOg3DFjt1xxY5jcMUOccWOY3DFDnHFzr3pvlauWLrN/jeIsKf/DPbhih26e6jVamzfvh1arRZZWVn44IMPMGjQILM23377LTp06GBzUYeIiIiIiIiIauZcexMi2xgMBrz//vsYPnw4pFIpHn/8ccycObPq540aNYLBYMDmzZvrLiQRERERERHRXYQrdshuZDIZTpw4gdLSUly9ehXLli2Dh4fpTlKpqalIS0tDmzZtbvEoRERERERERAAggs7gVK+/bFFYWIhBgwZBLpcjPDwca9eutdquvLwcL7zwAgIDA+Hj44P+/fsjKyvLatvqWNghIiIiIiIiInKQSZMmwcXFBbm5uVizZg1efPFFnD9/3qLdl19+iWPHjuHMmTPIzs6Gt7c3pkyZUuvjs7BDREREREREROQASqUSGzZswIcffgiFQoFu3bphwIABWLVqlUXblJQU9O7dG4GBgXBzc8Pw4cOtFoBuxsIOEREREREREdU7BgB6ONXrr7y8PLRv377qa9GiRWZ/Q2JiIpydndG0adOq7z3wwANWCzYTJkzAkSNHkJ2dDZVKhTVr1qBv37619hMvnkxEREREREREdAf8/f1vebvzsrIys2vPAoCnpydKS0st2jZp0gRhYWEICQmBWCxGy5YtsWDBglozcMUOEREREREREZEDKBQKlJSUmH2vpKQE7u7uFm0nTZqE8vJyFBQUQKlUIjY21qYVOyzsEBEREREREVG9pIOoXn/VpmnTptBqtUhKSqr63unTpxEdHW3RNi4uDmPHjoWPjw9cXV0xZcoU/Pnnn8jPz7/lc7CwQ0RERERERETkAHK5HLGxsXjvvfegVCpx5MgRbNmyBU8//bRF2w4dOmDlypUoLi5GZWUlvvnmGwQHB8PPz++Wz8HCDhERERERERGRg3zzzTdQq9UICAjAyJEj8e233yI6Ohq///47FApFVbtPP/0Ubm5uaNKkCfz9/bF9+3Zs2rSp1sfnxZOJiIiIiIiIqN4xGETQGYS/HsXHxwebN2+2+P5DDz2EsrKyqn/7+vpizZo1t/34wu8hIiIiIiIiIqJ7FAs7REREREREREQCxVOxqP4zGGCorKjrFDZZ2SysriPYLHPDfXUdwWahg8/XdQSbiZyFM6yKG4bWdQSb6XPz6jqCTQzl5XUdwWbiwIC6jmAzbVZ2XUcgspmuqKiuI9hM7OlR1xHuSmIf77qOYDN9SVntjeoJoXwegKGuA9C9SDifQIiIiIiIiIjonqK34Zbi9zqeikVEREREREREJFAs7BARERERERERCRRPxSIiIiIiIiKiescAQMf1KLViDxERERERERERCRQLO0REREREREREAsVTsYiIiIiIiIioHhJBZ+B6lNqwh4iIiIiIiIiIBIqFHSIiIiIiIiIigeKpWERERERERERU7xgA6LkepVbsISIiIiIiIiIigWJhh4iIiIiIiIhIoHgqFhERERERERHVSzqDqK4j1HtcsUNEREREREREJFAs7BARERERERERCRQLO0REREREREREAsVr7FCVF154ASEhIXj33XfrOsodcffSYupnGWj3SBmKC8VYNrcB9m/yttLSgAnTr6DPyEIAwM4ffLBkdgMAxnM3I6PV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ToUoKZZg+RdROLA9ENFtizDz2zMY/ODDVVnHT72M3oONdxvdtaEBls6PAiDCAx2vYdI7CfALLIdGLcaF055Y8lkUstNty6orsLxblDX1oV9tVS+yWtths5bTDvut89Yn4YEu5qfqvzEkCmeO1T5n/aHfgxKDbdsA1S4k2gvP//hw7Q3r0Nax2WY3M6oLLOzUY2vWrMH8+fNx4cIFuLu7o127dpg+fTp+++23Ggs7zs7OyM7OxmuvvYb9+/ejvLwczZo1w6xZs9CzZ89/VNh58803sWbNGhQXFyMwMBD//e9/MXHiRADAd999hw8++ABqtRqLFi1CQECA1cIOADz77LP44YcfkJubC4Wi9gq9rYWd+sDWwk59YGthpz6wtbBTH9ha2KG7k62FnfrA1sJOfWBrYac+sPWDcn1ga2GnPrC1sFMf2FrYqQ8Etb3aWNipD2wt7NBtsqGwUx+wsGNfLOzYhoUd+tfNnDkTiYmJZnfIuhUWdhyDhR3HYGHn3sbCjmOwsOMYLOw4Bgs7jsHCDrGwc29iYcc2wtlTortCYWEhlixZglWrVtV1FCIiIiIiIqrHDAD0vFtZrYRzSIEE7/vvv0dYWBj69u2Lhx+u31VXIiIiIiIiIiHgih361zz33HN47rnn6joGERERERER0V2DhR0iIiIiIiIiqodE0Bl4olFt2ENERERERERERALFwg4RERERERERkUDxVCwiIiIiIiIiqncMAPQG3hWrNlyxQ0REREREREQkUCzsEBEREREREREJFAs7RERERERERFQv6eBUr79sUVhYiEGDBkEulyM8PBxr16612q5v375QKBRVXy4uLmjZsmWtj89r7BAREREREREROcikSZPg4uKC3NxcxMXF4fHHH8cDDzyA6Ohos3Y7duww+/ejjz6K7t271/r4XLFDREREREREROQASqUSGzZswIcffgiFQoFu3bphwIABWLVq1S1/LzU1Fb///jvGjBlT63NwxQ4RERERERER1TsGiAR/V6zExEQ4OzujadOmVd974IEHcPDgwVv+3sqVK/HQQw+hUaNGtT4HV+wQEREREREREd2BvLw8tG/fvupr0aJFZj8vKyuDh4eH2fc8PT1RWlp6y8dduXIlxo4da1MGrtghIiIiIiIiIroD/v7+OHnyZI0/VygUKCkpMfteSUkJ3N3da/ydw4cPIycn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ERER1T8GEe+KZQOu2CEiIiIiIiIiEigWdoiIiIiIiIiIBIqnYhERERERERFRvWMAoAdPxaoNV+wQEREREREREQkUCztERERERERERALFwg4RERERERERkUDxGjtEREREREREVC/xdue144odIiIiIiIiIiKBYmGHiIiIiIiIiEigeCoWEREREREREdU7BvBULFtwxQ4RERERERERkUCxsENERERERERE9H/27ju+qer/4/grTVe6aWkLHXSwhIrIni6goKAoU6YKboZbREFEBRTcimxlI8MyFUSmgz1kFjpoaWlLC23pnhm/PwJJQ4JUSGnz+36ej0ceD2hOb945PTnn5txz77VRciqWEEIIIYQQQgghaiQ5FevmZMWOEEIIIYQQQgghhI2SiR1hFW5ubiQkJFRvBk8178+NY/2ZIyzec5wHH8+6QUkdI8dfYPWxo6w+dpSR4y+gvyyX3iufnGfBzpNsTjxEZP/MKsnq7qVm0oJENsSdYMmB0zz0xJUbZn32vTTWnDrJmlMnefa9NJOs4RFFzNwSw4b448zcEkN4RJHVs7q5lfL+e3+yfs0qFv+wngcfOG+x3D3NMpg+dTtRK9eweMEGs+enT93OymVRRK1azaxvN9O+XYrVs7p7qZn0QyIb4k+y5GA0D/X5l3qdkMaaU6dYc+oUz064vl6LmflbLBvOnWDmb7GERxRbPaubp5r3551j/dl/WLz3JA8+nn3DrCPfTWH18WOsPn6Mke+mmGR95dMkFuw6xebzR6qkvWpydVx4U8PZTmrieqnJ3aK9YdniMzrOP6fmbGc1sZFqslcYyxYd15H4lJqY+9QkPKmm6B/dDbdzq9w81bw/O5Z1pw6x6K9/eLD3jepDx8h3kll15Airjhxh5DvJXKvTwLBiJs2NYeWhI6w+epgpi84SGFYVf/9yJn4Xzdqje1i04yAPPnrphllHvJnIyv37WLl/HyPeTKTi3/+aLo9nsPnsX/Ton271rLb0uZKsktVW+lawrXqVrFXUXj3KmDD9MFG7f2Ph+p080D31hllHjD7DT7//zk+//86I0WcMWSPuzebnXb+ZPH498CsdH7po1ay2su9qS/sCQliTTOzUIKGhoahUKtzc3AyPMWPGVHesSikoKCA8PLxaM4z5OAl1uYJBre5lxqvhjJ2SREhD806455DLdOyew6iH7+blHnfT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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "predicted = classifier.predict(X_test)\n", + "\n", + "y_test_aligned = [p if p in y else y[0] for p, y in zip(predicted, y_test)]\n", + "\n", + "print(metrics.classification_report(y_test_aligned, predicted))\n", + "metrics.ConfusionMatrixDisplay.from_predictions(\n", + " y_true=y_test_aligned, y_pred=predicted, xticks_rotation=\"vertical\", normalize=\"pred\", values_format='.2f'\n", + ")\n", + "None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path = model_key\n", + "dump(p, path)\n", + "# LargeFile(path, \"w\", keep_last_n=1).push(path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for X, y in zip(X_test[:50], y_test):\n", + " print(', '.join(y))\n", + " pprint(predict(X))\n", + " print()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "b1d2807ee4cacf26d55377aa5a9287d91820f3fc1d1a8b3c12cb9914c0c55aa2" + }, + "kernelspec": { + "display_name": "Python 3.9.2 ('.env': venv)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/good_ai/.gitignore b/good_ai/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/good_ai/.gitignore @@ -0,0 +1 @@ +build diff --git a/good_ai/README.md b/good_ai/README.md new file mode 100644 index 0000000..19c5c98 --- /dev/null +++ b/good_ai/README.md @@ -0,0 +1,33 @@ +# **S**coutinScience **U**tilitie**S** for text processing [![Lint and test ScoutinScience utilities](https://github.com/ScoutinScience/platform/actions/workflows/sus-general.yaml/badge.svg)](https://github.com/ScoutinScience/platform/actions/workflows/sus-general.yaml) + +> amogus + +## Exports + +- [clean](src/sus/clean.py) +- [unique](src/sus/unique.py) +- [parallel_map](src/sus/parallel_map.py) +- [match_names](src/sus/match_names/match_names.py) +- [evaluate_ranking](src/sus/evaluate_ranking/evaluate_ranking.py) + +### Requires loading spacy model + +> This is automatic but will require some time. + +> Add this to the Dockerfile for caching the spaCy model: +> +> ```docker +> RUN pip install --no-cache-dir en-core-web-lg@https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.2.0/en_core_web_lg-3.2.0-py3-none-any.whl +> ``` + +- [spacy model (nlp)](src/sus/nlp.py) +- [get_sentences](src/sus/get_sentences.py) +- [lemmatize_text](src/sus/lemmatize_text.py) +- [lemmatize_token](src/sus/lemmatize_token.py) +- [publication TEI](src/sus/publication_tei/publication_tei.py) + +## Development + +- Optional booleans must have a default value of `False`. +- No imports in top-level `__init__.py`, in order to not load anything unnecessary automatically +- Should only be updated through a PR diff --git a/good_ai/example_secrets.ini b/good_ai/example_secrets.ini new file mode 100644 index 0000000..2bb053f --- /dev/null +++ b/good_ai/example_secrets.ini @@ -0,0 +1,6 @@ +[DEFAULT] +aws_region_name = your_region_like_eu-west-2 +aws_access_key_id = YOUR_ACCESS_KEY_ID +aws_secret_access_key = YOUR_VERY_SECRET_ACCESS_KEY +large_files_bucket_name = create_a_bucket_and_put_its_name_here +endpoint_url = this is optional, for backblaze, use this: https://s3.us-west-002.backblazeb2.com diff --git a/good_ai/open_s3.md b/good_ai/open_s3.md new file mode 100644 index 0000000..8263b5b --- /dev/null +++ b/good_ai/open_s3.md @@ -0,0 +1,119 @@ +# [open(S3)](https://pypi.org/project/open-large/) + +Storing, versioning, and downloading files from S3 made as easy as using `open()` in Python. Caching included. + +## Motivation + +Oftentimes, especially when working with data-heavy applications, large files can proliferate in a repository. Version controlling them is an obvious next step, however, GitHub's git LFS implementation [doesn't support the deletion](https://docs.github.com/en/repositories/working-with-files/managing-large-files/removing-files-from-git-large-file-storage#git-lfs-objects-in-your-repository) of large files, making it easy for them to eat-up the LFS quota and explode the size of your repos. + +## Solution + +``` +pip install open-large +``` + +### Simple example + +```python +from open_s3 import LargeFile + +LargeFile.configure_credentials({ + "aws_region_name": "your_region_like_eu-west-2", + "aws_access_key_id": "YOUR_ACCESS_KEY_ID", + "aws_secret_access_key": "YOUR_VERY_SECRET_ACCESS_KEY", + "large_files_bucket_name": "create_a_bucket_and_put_its_name_here", +}) + +# Creates a new version and deletes the older version leaving the 3 most recently used intact +with LargeFile("test.txt", "w", keep_last_n=3) as f: + for i in range(100000): + f.write('test\n') + +# By default the latest version is returned +# but an optional `version` keyword argument can be provided as well +with LargeFile("test.txt", "r") as f: + print(f.readlines()[0]) +``` + +> Automatically creates a file, writes to it, uploads it to S3, and then queries the most recent version of it. +> In this case, the latest version is already in the local cache, no download is required. + +### More details + +`LargeFile` behaves like an opened file (in the background it is a temp file after all). Binary reading and writing is supported along with the [different keywords](https://docs.python.org/3/library/functions.html#open) `open()` accepts. + +The local cache can be configured with these properties: + +```python +LargeFile.cache_path = Path('.cache') +LargeFile.max_cache_size = "30 GB" +``` + +#### I only need a path + +In case you only need a path to the "remote" file, this pattern can be applied: + +```python +path_to_model = LargeFile("folder-of-my-bert-model", version=31).get() +``` + +> This will first download the file/folder into your local cache folder. Then, it returns a `Path` object to the local version. Which can be turned into a string with `str(path_to_model)`. + +The same approach works for uploads: + +```python +LargeFile("folder-of-my-bert-model").push('path_to_local/folder_or_file') +``` + +> This way, both regular files and folders can be handled. The uploaded file is called **folder-of-my-bert-model**, the local name is ignored. + +Lastly, all version of the remote object can be deleted by calling `LargeFile("my-file").delete()`. It will still reside in your local cache afterwards, its deletion will happen next time your local cache has to be pruned. + +### Command-line example + +The package can be used as a module from the command-line to give you more flexibility. + +#### Setup + +Create an .ini file (or use _~/.aws/credentials_). It may look like this: + +```ini +[DEFAULT] +aws_region_name = your_region_like_eu-west-2 +aws_access_key_id = YOUR_ACCESS_KEY_ID +aws_secret_access_key = YOUR_VERY_SECRET_ACCESS_KEY +large_files_bucket_name = my_large_files +endpoint_url = this is optional, for backblaze, use this: https://s3.us-west-002.backblazeb2.com +``` + +> Just like in [example secrets](example_secrets.ini). + +#### Print the expected options + +```sh +python3 -m open_s3 --help +``` + +#### Upload some files + +```sh +python3 -m open_s3 --secrets secrets.ini --push my_first_file.json folder/my_second_file my_folder +``` + +> Only the filename is used as the S3 name, the rest of the path is ignored. + +#### Download some files to the local cache + +This can be useful when building a Docker image for example. This way, the files can already reside inside the container and need not be downloaded later. + +```sh +python3 -m open_s3 --secrets ~/.aws/credentials --cache my_first_file.json:3 my_second_file my_folder:0 +``` + +> Versions may be specified by using `:`-s. + +#### Delete remote files + +```sh +python3 -m open_s3 --secrets ~/.aws/credentials --delete my_first_file.json +``` diff --git a/good_ai/pyproject.toml b/good_ai/pyproject.toml new file mode 100644 index 0000000..4faf030 --- /dev/null +++ b/good_ai/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = [ + "setuptools>=42", + "setuptools-git", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/good_ai/requirements.txt b/good_ai/requirements.txt new file mode 100644 index 0000000..4e916a5 --- /dev/null +++ b/good_ai/requirements.txt @@ -0,0 +1,11 @@ +unidecode >= 1.3.0 +multiprocess >= 0.70.0.0 +tqdm >= 4.0.0 +psutil >= 5.9.0 +beautifulsoup4 >= 4.10.0 +lxml >= 4.6.0 +spacy >= 3.2.0 +pydantic >= 1.8.0 +scikit-learn >= 1.0.0 +matplotlib >= 3.5.0 +numpy >= 1.22.0 diff --git a/good_ai/setup.cfg b/good_ai/setup.cfg new file mode 100644 index 0000000..0fc9030 --- /dev/null +++ b/good_ai/setup.cfg @@ -0,0 +1,39 @@ +[metadata] +name = sus +version = 0.0.3 +author = ScoutinScience B.V. +author_email = andras@scoutinscience.com +description = [S]coutinScience [u]tilitie[s]: reusable utilities for text processing +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/ScoutinScience/platform +project_urls = + Bug Tracker = https://github.com/ScoutinScience/platform/issues +classifiers = + Programming Language :: Python :: 3 + Operating System :: OS Independent + +[options] +package_dir = + = src +packages = find: +include_package_data = True +python_requires = >=3.8 +install_requires = + click < 8.1.0 + unidecode >= 1.3.0 + multiprocess >= 0.70.0.0 + tqdm >= 4.0.0 + psutil >= 5.9.0 + beautifulsoup4 >= 4.10.0 + lxml >= 4.6.0 + spacy >= 3.2.0 + pydantic >= 1.8.0 + scikit-learn >= 1.0.0 + matplotlib >= 3.5.0 + numpy >= 1.22.0 + langcodes[data] >= 3.3.0 + langdetect >= 1.0.9 + +[options.packages.find] +where = src diff --git a/good_ai/src/__init__.py b/good_ai/src/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/good_ai/src/__init__.py @@ -0,0 +1 @@ + diff --git a/good_ai/src/open_s3/__init__.py b/good_ai/src/open_s3/__init__.py new file mode 100644 index 0000000..3e78b20 --- /dev/null +++ b/good_ai/src/open_s3/__init__.py @@ -0,0 +1 @@ +from .large_file import LargeFile diff --git a/good_ai/src/open_s3/__main__.py b/good_ai/src/open_s3/__main__.py new file mode 100644 index 0000000..9e5a5b8 --- /dev/null +++ b/good_ai/src/open_s3/__main__.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import logging +from pathlib import Path + +from large_file import LargeFile +from parse_arguments import parse_arguments + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + parser, args = parse_arguments() + + LargeFile.configure_credentials_from_file(args.secrets) + + if not args.cache and not args.push and not args.delete: + logging.warn("No action required.") + parser.print_help() + try: + if args.cache: + for c in args.cache: + split = c.split(":") + file_name = split[0] + version = None if len(split) == 1 else int(split[1]) + LargeFile(file_name, "r", version=version).get() + + if args.push: + for p in args.push: + path = Path(p) + LargeFile(path.name, "w").push(path) + + if args.delete: + for f in args.delete: + LargeFile(f).delete() + except Exception as e: + logging.error(e) diff --git a/good_ai/src/open_s3/helper/__init__.py b/good_ai/src/open_s3/helper/__init__.py new file mode 100644 index 0000000..ba3ed01 --- /dev/null +++ b/good_ai/src/open_s3/helper/__init__.py @@ -0,0 +1,2 @@ +from .progress_bar import DownloadProgressBar, UploadProgressBar +from .human_readable_to_byte import human_readable_to_byte diff --git a/good_ai/src/open_s3/helper/human_readable_to_byte.py b/good_ai/src/open_s3/helper/human_readable_to_byte.py new file mode 100644 index 0000000..9be0fe0 --- /dev/null +++ b/good_ai/src/open_s3/helper/human_readable_to_byte.py @@ -0,0 +1,31 @@ +import re + + +def human_readable_to_byte(size: str) -> int: + """Case is ignored, kb, kB, Kb, and KB are all treated as kilobyte.""" + + if size.strip() == "0": + return 0 + + possible_units = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + units_re = "|".join(possible_units) + regex = re.compile( + rf""" + \s* # trim + (?P\d+(.\d+)?) # get scalar, it might be a float + \s* # ignore optional whitespace + (?P{units_re}) # capture the unit + """, + flags=re.VERBOSE | re.IGNORECASE, + ) + + match = regex.match(size) + if not match: + raise ValueError(f'Could not find values in "{size}"') + + results = match.groupdict() + + scalar = float(results["scalar"]) + idx = possible_units.index(results["unit"].upper()) + factor = 1024 ** idx + return round(scalar * factor) diff --git a/good_ai/src/open_s3/helper/progress_bar.py b/good_ai/src/open_s3/helper/progress_bar.py new file mode 100644 index 0000000..ecd01e4 --- /dev/null +++ b/good_ai/src/open_s3/helper/progress_bar.py @@ -0,0 +1,40 @@ +import os +import threading +from logging import Logger +from typing import IO, Any, Optional + +from tqdm.auto import tqdm +from pathlib import Path + +import os +import sys +import threading + + +class ProgressBar: + def __init__(self, file_size: int, logger: Logger, prefix: str): + self._file_size = file_size + self._logger = logger + self._prefix=prefix + + self._seen_so_far = 0 + self._lock = threading.Lock() + + def __call__(self, bytes_amount: int): + with self._lock: + self._seen_so_far += bytes_amount + percentage = (self._seen_so_far / float(self._file_size)) * 100 + size_length = len(str(self._file_size)) + progress = str(self._seen_so_far).rjust(size_length) + self._logger.info(f"{self._prefix} {progress}/{self._file_size} bytes ({percentage:.1f}%)") + + +class DownloadProgressBar(ProgressBar): + def __init__(self, name: str, size: int, logger: Logger): + super().__init__(file_size=size, logger=logger, prefix=f'Downloading {name}') + + +class UploadProgressBar(ProgressBar): + def __init__(self, path: Path, logger: Logger): + size = os.path.getsize(path) + super().__init__(file_size=size, logger=logger, prefix=f'Uploading {path.name}') diff --git a/good_ai/src/open_s3/large_file.py b/good_ai/src/open_s3/large_file.py new file mode 100644 index 0000000..eff3931 --- /dev/null +++ b/good_ai/src/open_s3/large_file.py @@ -0,0 +1,346 @@ +import configparser +import logging +import os +import shutil +import tempfile +from pathlib import Path +from types import TracebackType +from typing import IO, Any, Dict, List, Optional, Type, Union +import boto3 +from botocore.exceptions import ClientError + +from helper import DownloadProgressBar, UploadProgressBar, human_readable_to_byte + +logger = logging.getLogger("open_s3") + + +class LargeFile: + """ + Store large files in S3. Use local cache for speed up. + + Examples: + + ``` + with LargeFile("test.txt", "w", keep_last_n=3) as f: + for i in range(1000000): + f.write('test\n') + + with LargeFile("test.txt", "r") as f: + print(f.readlines()[0]) + + path_to_cached_text_file = LargeFile("test.txt", version=0).get() + ``` + + By default, files are stored in the ".cache" folder and the + least recently use is deleted after the overall size reaches 30 GBs. + + Change it with the following properties. + + ``` + LargeFile.cache_path = Path(".cache") + LargeFile.max_cache_size = "30GB" + ``` + """ + + region_name = None + access_key_id = None + secret_access_key = None + bucket_name = None + endpoint_url = None + + cache_path = Path(".cache") + max_cache_size: Optional[str] = "30GB" + + def __init__( + self, + name: str, + mode: str = "r", + *, + buffering: int = -1, + encoding: Optional[str] = None, + errors: Optional[str] = None, + newline: Optional[str] = None, + version: Optional[int] = None, + keep_last_n: Optional[int] = None, + offline_mode: bool = False + ): + self._name: str = name + self._version = version + self._mode: str = mode + self._keep_last_n = keep_last_n + self._offline_mode = offline_mode + + self._buffering = buffering + self._encoding = encoding + self._errors = errors + self._newline = newline + + LargeFile.cache_path.mkdir(parents=True, exist_ok=True) + + self._find_versions() + self._check_mode_and_set_version() + + @classmethod + def configure_credentials( + cls, + *, + aws_region_name: str, + aws_access_key_id: str, + aws_secret_access_key: str, + large_files_bucket_name: str, + endpoint_url: Optional[str] = None, + **_: Dict[str, Any], + ) -> None: + cls.region_name = aws_region_name + cls.access_key_id = aws_access_key_id + cls.secret_access_key = aws_secret_access_key + cls.bucket_name = large_files_bucket_name + cls.endpoint_url = endpoint_url + + @classmethod + def configure_credentials_from_file( + cls, + secrets_path: Union[Path, str], + ) -> None: + + if isinstance(secrets_path, str): + secrets_path = Path(secrets_path) + + if not secrets_path.exists(): + raise FileNotFoundError(secrets_path.resolve()) + + credentials = configparser.ConfigParser() + credentials.read(secrets_path) + credentials.default_section + cls.configure_credentials(**credentials[credentials.default_section]) + + def __enter__(self) -> IO: + self._file: IO[Any] = ( + tempfile.NamedTemporaryFile( + mode=self._mode, + buffering=self._buffering, + encoding=self._encoding, + newline=self._newline, + errors=self._errors, + delete=False, + prefix="large-file-", + ) + if "w" in self._mode + else open( + self.get(), + mode=self._mode, + buffering=self._buffering, + encoding=self._encoding, + newline=self._newline, + errors=self._errors, + ) + ) + + return self._file + + def __exit__( + self, + type: Optional[Type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> bool: + self._file.close() + + if type is None: + if "w" in self._mode: + self.push(Path(self._file.name)) + os.unlink(self._file.name) + else: + logger.exception("Could not finish operation.") + + return True + + @property + def version_ids(self) -> List[int]: + return [self._get_version_from_key(key) for key in self._versions] + + def get(self, hide_progress: bool = False) -> Path: + key = next( + key + for key in self._versions + if self._get_version_from_key(key) == self._version + ) + + destination = self.cache_path / self._local_name + if not destination.exists(): + logger.info( + f"File {self._local_name} does not exist locally, starting download from S3" + ) + + with tempfile.TemporaryDirectory() as tmp: + tmp_file_archive = Path(tmp) / f"{self._local_name}.tar.gz" + + size = self._client.head_object(Bucket=self.bucket_name, Key=key)['ContentLength'] + self._client.download_file(Bucket=self.bucket_name, Key=key, Filename=str(tmp_file_archive), Callback=None if hide_progress else DownloadProgressBar(size=size, name=key, logger=logger)) + logger.info(f"Decompressing {self._local_name}") + shutil.unpack_archive(str(tmp_file_archive), tmp, "gztar") + tmp_file = Path(tmp) / self._local_name + shutil.move(str(tmp_file), str(destination)) + else: + logger.info(f"File {self._local_name} found in cache") + + return destination + + def push(self, path: Union[Path, str], hide_progress: bool = False) -> None: + if isinstance(path, str): + path = Path(path) + + with tempfile.TemporaryDirectory() as tmp: + if path.is_file(): + logger.info(f"Copying file for {self._local_name}") + copy = shutil.copy + else: + logger.info(f"Copying directory for {self._local_name}") + copy = shutil.copytree + + try: + # Make local copy in the cache + copy(str(path), str(self.cache_path / self._local_name)) + except shutil.SameFileError: + pass # No worries + + copy(str(path), str(Path(tmp) / self._local_name)) + + logger.info(f"Compressing {self._local_name}") + shutil.make_archive( + str(Path(tmp) / self._local_name), + "gztar", + tmp, + ) + + logger.info(f"Uploading {self._local_name} to S3 from {path}") + + file_to_be_uploaded = Path(tmp) / f"{self._local_name}.tar.gz" + self._client.upload_file(Filename=str(file_to_be_uploaded), Bucket=self.bucket_name, Key=self._s3_name, Callback=None if hide_progress else UploadProgressBar(file_to_be_uploaded, logger=logger)) + + self.clean_up() + + def delete(self) -> None: + self._keep_last_n = 0 + self._delete_old_versions_from_s3() + + def clean_up(self) -> None: + self._delete_old_versions_from_s3() + self._delete_old_versions_from_disk() + + def _create_client(self) -> None: + if ( + self.region_name is None + or self.access_key_id is None + or self.secret_access_key is None + or self.bucket_name is None + ): + raise ValueError( + "Please configure the S3 access options by calling LargeFile.configure_credentials or set offline_mode=True in the constructor." + ) + + self._client = boto3.client('s3', aws_access_key_id=self.access_key_id, aws_secret_access_key=self.secret_access_key, region_name=self.region_name, endpoint_url=self.endpoint_url) + + def _find_versions(self) -> None: + if self._offline_mode: + self._fetch_versions_from_cache() + else: + self._create_client() + self._fetch_versions_from_s3() + + if self._versions: + logger.info(f"Found versions: {self.version_ids}") + else: + logger.info("No versions found") + + def _fetch_versions_from_cache(self) -> None: + logger.info(f"Fetching offline versions of {self._name}") + + self._versions = [ + path + for path in self.cache_path.glob(f'{self._local_name}-*') + ] + + def _fetch_versions_from_s3(self) -> None: + logger.info(f"Fetching online versions of {self._name}") + found_objects = self._client.list_objects_v2( + Bucket=self.bucket_name, Prefix=self._name + ) + self._versions = ( + sorted( + o["Key"] + for o in found_objects["Contents"] + ) + if "Contents" in found_objects + else [] + ) + + def _check_mode_and_set_version(self) -> None: + if "+" in self._mode: + raise ValueError("Read-write mode is not allowed.") + + if "w" in self._mode: + if self._version is not None: + raise ValueError("Providing a version is not allowed in write mode.") + + self._version = self.version_ids[-1] + 1 if self.version_ids else 0 + + elif "r" in self._mode: + if not self.version_ids: + raise FileNotFoundError( + f"File {self._name} not found. No versions are available." + ) + + if self._version is None: + self._version = self.version_ids[-1] + logger.info(f"Lastest version of {self._local_name} is {self._version}") + + elif self._version not in self.version_ids: + raise FileNotFoundError( + f"File {self._name} not found with version {self._version}. Available versions: {self.version_ids}" + ) + else: + raise ValueError("Unsupported file mode.") + + @property + def _local_name(self) -> str: + return f"{self._name}-{self._version}" + + @property + def _s3_name(self) -> str: + return f"{self._name}/{self._version}" + + @staticmethod + def _get_version_from_key(key: Union[str, Path]) -> int: + if isinstance(key, Path): + return int(key.name.split("-")[-1]) + return int(key.split("/")[-1]) + + + def _delete_old_versions_from_s3(self) -> None: + if self._keep_last_n is not None: + for key in (self._versions[: -self._keep_last_n] if self._keep_last_n > 0 else self._versions): + logger.info( + f"Removing old version (keep_last_n={self._keep_last_n}): {key}" + ) + self._client.delete_object(Bucket=self.bucket_name, Key=key) + + def _delete_old_versions_from_disk(self) -> None: + self.cache_path.mkdir(parents=True, exist_ok=True) + + if self.max_cache_size is None: + return + + allowed_size = human_readable_to_byte(self.max_cache_size) + assert allowed_size >= 0 + + least_recently_read = sorted( + [f for f in self.cache_path.glob("*")], key=lambda f: f.stat().st_atime + ) + + while sum(os.path.getsize(f) for f in least_recently_read) > allowed_size: + file = least_recently_read.pop(0) + logger.info( + f"Deleting file from cache to meet quota (max_cache_size={self.max_cache_size}): {file}" + ) + os.unlink(file) \ No newline at end of file diff --git a/good_ai/src/open_s3/parse_arguments.py b/good_ai/src/open_s3/parse_arguments.py new file mode 100644 index 0000000..acb6966 --- /dev/null +++ b/good_ai/src/open_s3/parse_arguments.py @@ -0,0 +1,48 @@ +from argparse import ArgumentParser, Namespace +from typing import Tuple + + +def parse_arguments() -> Tuple[ArgumentParser, Namespace]: + parser = ArgumentParser( + description="Store and version large files in S3; open them like regular files. Caching included.", + ) + + parser.add_argument( + "-s", + "--secrets", + type=str, + help="path to an .ini configration file with your S3 credentials", + required=True, + ) + + parser.add_argument( + "-c", + "--cache", + nargs="+", + type=str, + help="download file into local cache, example: file_name:version", + required=False, + ) + + parser.add_argument( + "-p", + "--push", + nargs="+", + type=str, + help="push a local file into S3 and set it as the most recent version", + required=False, + ) + + parser.add_argument( + "-d", + "--delete", + nargs="+", + type=str, + help="delete every version of file from S3", + required=False, + ) + + parser.print_usage = parser.print_help # type: ignore + args = parser.parse_args() + + return parser, args diff --git a/good_ai/src/sus.egg-info/PKG-INFO b/good_ai/src/sus.egg-info/PKG-INFO new file mode 100644 index 0000000..13a14e2 --- /dev/null +++ b/good_ai/src/sus.egg-info/PKG-INFO @@ -0,0 +1,50 @@ +Metadata-Version: 2.1 +Name: sus +Version: 0.0.3 +Summary: [S]coutinScience [u]tilitie[s]: reusable utilities for text processing +Home-page: https://github.com/ScoutinScience/platform +Author: ScoutinScience B.V. +Author-email: andras@scoutinscience.com +License: UNKNOWN +Project-URL: Bug Tracker, https://github.com/ScoutinScience/platform/issues +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: Operating System :: OS Independent +Requires-Python: >=3.8 +Description-Content-Type: text/markdown + +# **S**coutinScience **U**tilitie**S** for text processing [![Lint and test ScoutinScience utilities](https://github.com/ScoutinScience/platform/actions/workflows/sus-general.yaml/badge.svg)](https://github.com/ScoutinScience/platform/actions/workflows/sus-general.yaml) + +> amogus + +## Exports + +- [clean](src/sus/clean.py) +- [unique](src/sus/unique.py) +- [parallel_map](src/sus/parallel_map.py) +- [match_names](src/sus/match_names/match_names.py) +- [evaluate_ranking](src/sus/evaluate_ranking/evaluate_ranking.py) + +### Requires loading spacy model + +> This is automatic but will require some time. + +> Add this to the Dockerfile for caching the spaCy model: +> +> ```docker +> RUN pip install --no-cache-dir en-core-web-lg@https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.2.0/en_core_web_lg-3.2.0-py3-none-any.whl +> ``` + +- [spacy model (nlp)](src/sus/nlp.py) +- [get_sentences](src/sus/get_sentences.py) +- [lemmatize_text](src/sus/lemmatize_text.py) +- [lemmatize_token](src/sus/lemmatize_token.py) +- [publication TEI](src/sus/publication_tei/publication_tei.py) + +## Development + +- Optional booleans must have a default value of `False`. +- No imports in top-level `__init__.py`, in order to not load anything unnecessary automatically +- Should only be updated through a PR + + diff --git a/good_ai/src/sus.egg-info/SOURCES.txt b/good_ai/src/sus.egg-info/SOURCES.txt new file mode 100644 index 0000000..6703262 --- /dev/null +++ b/good_ai/src/sus.egg-info/SOURCES.txt @@ -0,0 +1,78 @@ +.gitignore +README.md +pyproject.toml +requirements.txt +setup.cfg +src/__init__.py +src/sus/__init__.py +src/sus/clean.py +src/sus/get_sentences.py +src/sus/lemmatize_text.py +src/sus/lemmatize_token.py +src/sus/nlp.py +src/sus/parallel_map.py +src/sus/unique.py +src/sus.egg-info/PKG-INFO +src/sus.egg-info/SOURCES.txt +src/sus.egg-info/dependency_links.txt +src/sus.egg-info/requires.txt +src/sus.egg-info/top_level.txt +src/sus/data/__init__.py +src/sus/data/american_spellings.py +src/sus/data/punctuations.py +src/sus/evaluate_ranking/__init__.py +src/sus/evaluate_ranking/draw_f1_iso_lines.py +src/sus/evaluate_ranking/evaluate_ranking.py +src/sus/external/__init__.py +src/sus/external/negspacy/README.md +src/sus/external/negspacy/__init__.py +src/sus/external/negspacy/negation.py +src/sus/external/negspacy/termsets.py +src/sus/external/pylatexenc/README.md +src/sus/external/pylatexenc/__init__.py +src/sus/external/pylatexenc/_util.py +src/sus/external/pylatexenc/version.py +src/sus/external/pylatexenc/latex2text/__init__.py +src/sus/external/pylatexenc/latex2text/__main__.py +src/sus/external/pylatexenc/latex2text/_defaultspecs.py +src/sus/external/pylatexenc/latexencode/__init__.py +src/sus/external/pylatexenc/latexencode/__main__.py +src/sus/external/pylatexenc/latexencode/_partial_latex_encoder.py +src/sus/external/pylatexenc/latexencode/_uni2latexmap.py +src/sus/external/pylatexenc/latexencode/_uni2latexmap_xml.py +src/sus/external/pylatexenc/latexencode/_unicode_to_latex_encoder.py +src/sus/external/pylatexenc/latexwalker/__init__.py +src/sus/external/pylatexenc/latexwalker/__main__.py +src/sus/external/pylatexenc/latexwalker/_defaultspecs.py +src/sus/external/pylatexenc/macrospec/__init__.py +src/sus/external/pylatexenc/macrospec/_argparsers.py +src/sus/language/__init__.py +src/sus/language/english_name_of_language.py +src/sus/language/is_english.py +src/sus/language/predict_language.py +src/sus/match_names/__init__.py +src/sus/match_names/config.py +src/sus/match_names/match_names.py +src/sus/match_names/name_parts.py +src/sus/publication_tei/__init__.py +src/sus/publication_tei/publication_tei.py +src/sus/publication_tei/models/__init__.py +src/sus/publication_tei/models/affiliation.py +src/sus/publication_tei/models/author.py +src/sus/publication_tei/models/element.py +src/sus/publication_tei/models/publication_metadata.py +src/sus/publication_tei/models/text.py +tests/__init__.py +tests/test_clean.py +tests/test_evaluate_ranking.py +tests/test_get_sentences.py +tests/test_language.py +tests/test_lemmatize_text.py +tests/test_lemmatize_token.py +tests/test_match_names.py +tests/test_parallel_map.py +tests/test_publication_tei.py +tests/test_unique.py +tests/data/10.1136_bmjspcare-2021-003026.pdf.tei.xml +tests/data/bad.tei.xml +tests/data/parsed.py \ No newline at end of file diff --git a/good_ai/src/sus.egg-info/dependency_links.txt b/good_ai/src/sus.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/good_ai/src/sus.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/good_ai/src/sus.egg-info/requires.txt b/good_ai/src/sus.egg-info/requires.txt new file mode 100644 index 0000000..1f9aec7 --- /dev/null +++ b/good_ai/src/sus.egg-info/requires.txt @@ -0,0 +1,14 @@ +click<8.1.0 +unidecode>=1.3.0 +multiprocess>=0.70.0.0 +tqdm>=4.0.0 +psutil>=5.9.0 +beautifulsoup4>=4.10.0 +lxml>=4.6.0 +spacy>=3.2.0 +pydantic>=1.8.0 +scikit-learn>=1.0.0 +matplotlib>=3.5.0 +numpy>=1.22.0 +langcodes[data]>=3.3.0 +langdetect>=1.0.9 diff --git a/good_ai/src/sus.egg-info/top_level.txt b/good_ai/src/sus.egg-info/top_level.txt new file mode 100644 index 0000000..da3f7c9 --- /dev/null +++ b/good_ai/src/sus.egg-info/top_level.txt @@ -0,0 +1 @@ +sus diff --git a/good_ai/src/sus/__init__.py b/good_ai/src/sus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/good_ai/src/sus/clean.py b/good_ai/src/sus/clean.py new file mode 100644 index 0000000..d7b5758 --- /dev/null +++ b/good_ai/src/sus/clean.py @@ -0,0 +1,64 @@ +import html +import logging +import re +import unicodedata + +import unidecode + +from .data import left_regular_punctuations, right_regular_punctuations +from .external.pylatexenc.latex2text import LatexNodes2Text + +logger = logging.getLogger("clean") +latex = LatexNodes2Text() + + +joined_left_punctuations = "".join(left_regular_punctuations).replace("]", r"\]") +joined_right_punctuations = "".join(right_regular_punctuations).replace("[", r"\[") + + +def clean( + text: str, + ignore_xml: bool = False, + ignore_latex: bool = False, + remove_brackets: bool = False, + convert_to_ascii: bool = False, +) -> str: + if not ignore_xml: + text = re.sub(r"<[^>]*>", " ", text) + text = html.unescape(text) + + if not ignore_latex: + text = text.replace("%", "\\%") # escape LaTeX comments before parsing as LaTeX + + try: + text = latex.latex_to_text(text, tolerant_parsing=True, strict_braces=False) + text = text.replace("%s", " ") + except: + logger.exception("Latex parsing error") + + if convert_to_ascii: + text = unicodedata.normalize("NFKD", text) + + try: + text.encode("ASCII", errors="strict") + except UnicodeEncodeError: + text = "".join([c for c in text if not unicodedata.combining(c)]) + text = unidecode.unidecode(text) + + if remove_brackets: + text = re.sub(r"\[[^\]]*\]", " ", text) + + # fix hypens: break- word => break-word + text = re.sub(r"(\S)-\s+", r"\1-", text) + text = re.sub(r"\s+-(\S)", r"-\1", text) + + # collapse whitespace + text = re.sub(r"\s+", " ", text) + + # fix punctuation + text = re.sub(rf" ([{joined_left_punctuations}])", r"\1", text) + text = re.sub(rf"([{joined_right_punctuations}]) ", r"\1", text) + + text = text.strip() + + return text diff --git a/good_ai/src/sus/data/__init__.py b/good_ai/src/sus/data/__init__.py new file mode 100644 index 0000000..00b12b2 --- /dev/null +++ b/good_ai/src/sus/data/__init__.py @@ -0,0 +1,6 @@ +from .american_spellings import american_spellings +from .punctuations import ( + left_regular_punctuations, + right_regular_punctuations, + sentence_ending_punctuations, +) diff --git a/good_ai/src/sus/data/american_spellings.py b/good_ai/src/sus/data/american_spellings.py new file mode 100644 index 0000000..b30c82c --- /dev/null +++ b/good_ai/src/sus/data/american_spellings.py @@ -0,0 +1,1711 @@ +from typing import Dict + +# source: https://github.com/hyperreality/American-British-English-Translator/blob/master/data/american_spellings.json +american_spellings: Dict[str, str] = { + "accessorize": "accessorise", + "accessorized": "accessorised", + "accessorizes": "accessorises", + "accessorizing": "accessorising", + "acclimatization": "acclimatisation", + "acclimatize": "acclimatise", + "acclimatized": "acclimatised", + "acclimatizes": "acclimatises", + "acclimatizing": "acclimatising", + "accouterments": "accoutrements", + "eon": "aeon", + "eons": "aeons", + "aerogram": "aerogramme", + "aerograms": "aerogrammes", + "airplane": "aeroplane", + "airplanes": "aeroplanes", + "esthete": "aesthete", + "esthetes": "aesthetes", + "esthetic": "aesthetic", + "esthetically": "aesthetically", + "esthetics": "aesthetics", + "etiology": "aetiology", + "aging": "ageing", + "aggrandizement": "aggrandisement", + "agonize": "agonise", + "agonized": "agonised", + "agonizes": "agonises", + "agonizing": "agonising", + "agonizingly": "agonisingly", + "almanac": "almanack", + "almanacs": "almanacks", + "aluminum": "aluminium", + "amortizable": "amortisable", + "amortization": "amortisation", + "amortizations": "amortisations", + "amortize": "amortise", + "amortized": "amortised", + "amortizes": "amortises", + "amortizing": "amortising", + "amphitheater": "amphitheatre", + "amphitheaters": "amphitheatres", + "anemia": "anaemia", + "anemic": "anaemic", + "anesthesia": "anaesthesia", + "anesthetic": "anaesthetic", + "anesthetics": "anaesthetics", + "anesthetize": "anaesthetize", + "anesthetized": "anaesthetized", + "anesthetizes": "anaesthetizes", + "anesthetizing": "anaesthetizing", + "anesthetist": "anaesthetist", + "anesthetists": "anaesthetists", + "analog": "analogue", + "analogs": "analogues", + "analyze": "analyse", + "analyzed": "analysed", + "analyzes": "analyses", + "analyzing": "analysing", + "anglicize": "anglicise", + "anglicized": "anglicised", + "anglicizes": "anglicises", + "anglicizing": "anglicising", + "annualized": "annualised", + "antagonize": "antagonise", + "antagonized": "antagonised", + "antagonizes": "antagonises", + "antagonizing": "antagonising", + "apologize": "apologise", + "apologized": "apologised", + "apologizes": "apologises", + "apologizing": "apologising", + "appall": "appal", + "appalls": "appals", + "appetizer": "appetiser", + "appetizers": "appetisers", + "appetizing": "appetising", + "appetizingly": "appetisingly", + "arbor": "arbour", + "arbors": "arbours", + "archeological": "archaeological", + "archeologically": "archaeologically", + "archeologist": "archaeologist", + "archeologists": "archaeologists", + "archeology": "archaeology", + "ardor": "ardour", + "armor": "armour", + "armored": "armoured", + "armorer": "armourer", + "armorers": "armourers", + "armories": "armouries", + "armory": "armoury", + "artifact": "artefact", + "artifacts": "artefacts", + "authorize": "authorise", + "authorized": "authorised", + "authorizes": "authorises", + "authorizing": "authorising", + "ax": "axe", + "backpedaled": "backpedalled", + "backpedaling": "backpedalling", + "banister": "bannister", + "banisters": "bannisters", + "baptize": "baptise", + "baptized": "baptised", + "baptizes": "baptises", + "baptizing": "baptising", + "bastardize": "bastardise", + "bastardized": "bastardised", + "bastardizes": "bastardises", + "bastardizing": "bastardising", + "battleax": "battleaxe", + "balk": "baulk", + "balked": "baulked", + "balking": "baulking", + "balks": "baulks", + "bedeviled": "bedevilled", + "bedeviling": "bedevilling", + "behavior": "behaviour", + "behavioral": "behavioural", + "behaviorism": "behaviourism", + "behaviorist": "behaviourist", + "behaviorists": "behaviourists", + "behaviors": "behaviours", + "behoove": "behove", + "behooved": "behoved", + "behooves": "behoves", + "bejeweled": "bejewelled", + "belabor": "belabour", + "belabored": "belaboured", + "belaboring": "belabouring", + "belabors": "belabours", + "beveled": "bevelled", + "bevies": "bevvies", + "bevy": "bevvy", + "biased": "biassed", + "biasing": "biassing", + "binging": "bingeing", + "bougainvillea": "bougainvillaea", + "bougainvilleas": "bougainvillaeas", + "bowdlerize": "bowdlerise", + "bowdlerized": "bowdlerised", + "bowdlerizes": "bowdlerises", + "bowdlerizing": "bowdlerising", + "breathalyze": "breathalyse", + "breathalyzed": "breathalysed", + "breathalyzer": "breathalyser", + "breathalyzers": "breathalysers", + "breathalyzes": "breathalyses", + "breathalyzing": "breathalysing", + "brutalize": "brutalise", + "brutalized": "brutalised", + "brutalizes": "brutalises", + "brutalizing": "brutalising", + "busses": "buses", + "bussing": "busing", + "cesarean": "caesarean", + "cesareans": "caesareans", + "caliber": "calibre", + "calibers": "calibres", + "caliper": "calliper", + "calipers": "callipers", + "calisthenics": "callisthenics", + "canalize": "canalise", + "canalized": "canalised", + "canalizes": "canalises", + "canalizing": "canalising", + "cancelation": "cancellation", + "cancelations": "cancellations", + "canceled": "cancelled", + "canceling": "cancelling", + "candor": "candour", + "cannibalize": "cannibalise", + "cannibalized": "cannibalised", + "cannibalizes": "cannibalises", + "cannibalizing": "cannibalising", + "canonize": "canonise", + "canonized": "canonised", + "canonizes": "canonises", + "canonizing": "canonising", + "capitalize": "capitalise", + "capitalized": "capitalised", + "capitalizes": "capitalises", + "capitalizing": "capitalising", + "caramelize": "caramelise", + "caramelized": "caramelised", + "caramelizes": "caramelises", + "caramelizing": "caramelising", + "carbonize": "carbonise", + "carbonized": "carbonised", + "carbonizes": "carbonises", + "carbonizing": "carbonising", + "caroled": "carolled", + "caroling": "carolling", + "catalog": "catalogue", + "cataloged": "catalogued", + "catalogs": "catalogues", + "cataloging": "cataloguing", + "catalyze": "catalyse", + "catalyzed": "catalysed", + "catalyzes": "catalyses", + "catalyzing": "catalysing", + "categorize": "categorise", + "categorized": "categorised", + "categorizes": "categorises", + "categorizing": "categorising", + "cauterize": "cauterise", + "cauterized": "cauterised", + "cauterizes": "cauterises", + "cauterizing": "cauterising", + "caviled": "cavilled", + "caviling": "cavilling", + "centigram": "centigramme", + "centigrams": "centigrammes", + "centiliter": "centilitre", + "centiliters": "centilitres", + "centimeter": "centimetre", + "centimeters": "centimetres", + "centralize": "centralise", + "centralized": "centralised", + "centralizes": "centralises", + "centralizing": "centralising", + "center": "centre", + "centered": "centred", + "centerfold": "centrefold", + "centerfolds": "centrefolds", + "centerpiece": "centrepiece", + "centerpieces": "centrepieces", + "centers": "centres", + "channeled": "channelled", + "channeling": "channelling", + "characterize": "characterise", + "characterized": "characterised", + "characterizes": "characterises", + "characterizing": "characterising", + "checkbook": "chequebook", + "checkbooks": "chequebooks", + "checkered": "chequered", + "chili": "chilli", + "chimera": "chimaera", + "chimeras": "chimaeras", + "chiseled": "chiselled", + "chiseling": "chiselling", + "circularize": "circularise", + "circularized": "circularised", + "circularizes": "circularises", + "circularizing": "circularising", + "civilize": "civilise", + "civilized": "civilised", + "civilizes": "civilises", + "civilizing": "civilising", + "clamor": "clamour", + "clamored": "clamoured", + "clamoring": "clamouring", + "clamors": "clamours", + "clangor": "clangour", + "clarinetist": "clarinettist", + "clarinetists": "clarinettists", + "collectivize": "collectivise", + "collectivized": "collectivised", + "collectivizes": "collectivises", + "collectivizing": "collectivising", + "colonization": "colonisation", + "colonize": "colonise", + "colonized": "colonised", + "colonizer": "coloniser", + "colonizers": "colonisers", + "colonizes": "colonises", + "colonizing": "colonising", + "color": "colour", + "colorant": "colourant", + "colorants": "colourants", + "colored": "coloured", + "coloreds": "coloureds", + "colorful": "colourful", + "colorfully": "colourfully", + "coloring": "colouring", + "colorize": "colourize", + "colorized": "colourized", + "colorizes": "colourizes", + "colorizing": "colourizing", + "colorless": "colourless", + "colors": "colours", + "commercialize": "commercialise", + "commercialized": "commercialised", + "commercializes": "commercialises", + "commercializing": "commercialising", + "compartmentalize": "compartmentalise", + "compartmentalized": "compartmentalised", + "compartmentalizes": "compartmentalises", + "compartmentalizing": "compartmentalising", + "computerize": "computerise", + "computerized": "computerised", + "computerizes": "computerises", + "computerizing": "computerising", + "conceptualize": "conceptualise", + "conceptualized": "conceptualised", + "conceptualizes": "conceptualises", + "conceptualizing": "conceptualising", + "connection": "connexion", + "connections": "connexions", + "contextualize": "contextualise", + "contextualized": "contextualised", + "contextualizes": "contextualises", + "contextualizing": "contextualising", + "cozier": "cosier", + "cozies": "cosies", + "coziest": "cosiest", + "cozily": "cosily", + "coziness": "cosiness", + "cozy": "cosy", + "councilor": "councillor", + "councilors": "councillors", + "counseled": "counselled", + "counseling": "counselling", + "counselor": "counsellor", + "counselors": "counsellors", + "crenelated": "crenellated", + "criminalize": "criminalise", + "criminalized": "criminalised", + "criminalizes": "criminalises", + "criminalizing": "criminalising", + "criticize": "criticise", + "criticized": "criticised", + "criticizes": "criticises", + "criticizing": "criticising", + "crueler": "crueller", + "cruelest": "cruellest", + "crystallization": "crystallisation", + "crystallize": "crystallise", + "crystallized": "crystallised", + "crystallizes": "crystallises", + "crystallizing": "crystallising", + "cudgeled": "cudgelled", + "cudgeling": "cudgelling", + "customize": "customise", + "customized": "customised", + "customizes": "customises", + "customizing": "customising", + "decentralization": "decentralisation", + "decentralize": "decentralise", + "decentralized": "decentralised", + "decentralizes": "decentralises", + "decentralizing": "decentralising", + "decriminalization": "decriminalisation", + "decriminalize": "decriminalise", + "decriminalized": "decriminalised", + "decriminalizes": "decriminalises", + "decriminalizing": "decriminalising", + "defense": "defence", + "defenseless": "defenceless", + "defenses": "defences", + "dehumanization": "dehumanisation", + "dehumanize": "dehumanise", + "dehumanized": "dehumanised", + "dehumanizes": "dehumanises", + "dehumanizing": "dehumanising", + "demeanor": "demeanour", + "demilitarization": "demilitarisation", + "demilitarize": "demilitarise", + "demilitarized": "demilitarised", + "demilitarizes": "demilitarises", + "demilitarizing": "demilitarising", + "demobilization": "demobilisation", + "demobilize": "demobilise", + "demobilized": "demobilised", + "demobilizes": "demobilises", + "demobilizing": "demobilising", + "democratization": "democratisation", + "democratize": "democratise", + "democratized": "democratised", + "democratizes": "democratises", + "democratizing": "democratising", + "demonize": "demonise", + "demonized": "demonised", + "demonizes": "demonises", + "demonizing": "demonising", + "demoralization": "demoralisation", + "demoralize": "demoralise", + "demoralized": "demoralised", + "demoralizes": "demoralises", + "demoralizing": "demoralising", + "denationalization": "denationalisation", + "denationalize": "denationalise", + "denationalized": "denationalised", + "denationalizes": "denationalises", + "denationalizing": "denationalising", + "deodorize": "deodorise", + "deodorized": "deodorised", + "deodorizes": "deodorises", + "deodorizing": "deodorising", + "depersonalize": "depersonalise", + "depersonalized": "depersonalised", + "depersonalizes": "depersonalises", + "depersonalizing": "depersonalising", + "deputize": "deputise", + "deputized": "deputised", + "deputizes": "deputises", + "deputizing": "deputising", + "desensitization": "desensitisation", + "desensitize": "desensitise", + "desensitized": "desensitised", + "desensitizes": "desensitises", + "desensitizing": "desensitising", + "destabilization": "destabilisation", + "destabilize": "destabilise", + "destabilized": "destabilised", + "destabilizes": "destabilises", + "destabilizing": "destabilising", + "dialed": "dialled", + "dialing": "dialling", + "dialog": "dialogue", + "dialogs": "dialogues", + "diarrhea": "diarrhoea", + "digitize": "digitise", + "digitized": "digitised", + "digitizes": "digitises", + "digitizing": "digitising", + "disk": "disc", + "discolor": "discolour", + "discolored": "discoloured", + "discoloring": "discolouring", + "discolors": "discolours", + "disks": "discs", + "disemboweled": "disembowelled", + "disemboweling": "disembowelling", + "disfavor": "disfavour", + "disheveled": "dishevelled", + "dishonor": "dishonour", + "dishonorable": "dishonourable", + "dishonorably": "dishonourably", + "dishonored": "dishonoured", + "dishonoring": "dishonouring", + "dishonors": "dishonours", + "disorganization": "disorganisation", + "disorganized": "disorganised", + "distill": "distil", + "distills": "distils", + "dramatization": "dramatisation", + "dramatizations": "dramatisations", + "dramatize": "dramatise", + "dramatized": "dramatised", + "dramatizes": "dramatises", + "dramatizing": "dramatising", + "draft": "draught", + "draftboard": "draughtboard", + "draftboards": "draughtboards", + "draftier": "draughtier", + "draftiest": "draughtiest", + "drafts": "draughts", + "draftsman": "draughtsman", + "draftsmanship": "draughtsmanship", + "draftsmen": "draughtsmen", + "draftswoman": "draughtswoman", + "draftswomen": "draughtswomen", + "drafty": "draughty", + "driveled": "drivelled", + "driveling": "drivelling", + "dueled": "duelled", + "dueling": "duelling", + "economize": "economise", + "economized": "economised", + "economizes": "economises", + "economizing": "economising", + "edema": "edoema", + "editorialize": "editorialise", + "editorialized": "editorialised", + "editorializes": "editorialises", + "editorializing": "editorialising", + "empathize": "empathise", + "empathized": "empathised", + "empathizes": "empathises", + "empathizing": "empathising", + "emphasize": "emphasise", + "emphasized": "emphasised", + "emphasizes": "emphasises", + "emphasizing": "emphasising", + "enameled": "enamelled", + "enameling": "enamelling", + "enamored": "enamoured", + "encyclopedia": "encyclopaedia", + "encyclopedias": "encyclopaedias", + "encyclopedic": "encyclopaedic", + "endeavor": "endeavour", + "endeavored": "endeavoured", + "endeavoring": "endeavouring", + "endeavors": "endeavours", + "energize": "energise", + "energized": "energised", + "energizes": "energises", + "energizing": "energising", + "enroll": "enrol", + "enrolls": "enrols", + "enthrall": "enthral", + "enthralls": "enthrals", + "epaulet": "epaulette", + "epaulets": "epaulettes", + "epicenter": "epicentre", + "epicenters": "epicentres", + "epilog": "epilogue", + "epilogs": "epilogues", + "epitomize": "epitomise", + "epitomized": "epitomised", + "epitomizes": "epitomises", + "epitomizing": "epitomising", + "equalization": "equalisation", + "equalize": "equalise", + "equalized": "equalised", + "equalizer": "equaliser", + "equalizers": "equalisers", + "equalizes": "equalises", + "equalizing": "equalising", + "eulogize": "eulogise", + "eulogized": "eulogised", + "eulogizes": "eulogises", + "eulogizing": "eulogising", + "evangelize": "evangelise", + "evangelized": "evangelised", + "evangelizes": "evangelises", + "evangelizing": "evangelising", + "exorcize": "exorcise", + "exorcized": "exorcised", + "exorcizes": "exorcises", + "exorcizing": "exorcising", + "extemporization": "extemporisation", + "extemporize": "extemporise", + "extemporized": "extemporised", + "extemporizes": "extemporises", + "extemporizing": "extemporising", + "externalization": "externalisation", + "externalizations": "externalisations", + "externalize": "externalise", + "externalized": "externalised", + "externalizes": "externalises", + "externalizing": "externalising", + "factorize": "factorise", + "factorized": "factorised", + "factorizes": "factorises", + "factorizing": "factorising", + "fecal": "faecal", + "feces": "faeces", + "familiarization": "familiarisation", + "familiarize": "familiarise", + "familiarized": "familiarised", + "familiarizes": "familiarises", + "familiarizing": "familiarising", + "fantasize": "fantasise", + "fantasized": "fantasised", + "fantasizes": "fantasises", + "fantasizing": "fantasising", + "favor": "favour", + "favorable": "favourable", + "favorably": "favourably", + "favored": "favoured", + "favoring": "favouring", + "favorite": "favourite", + "favorites": "favourites", + "favoritism": "favouritism", + "favors": "favours", + "feminize": "feminise", + "feminized": "feminised", + "feminizes": "feminises", + "feminizing": "feminising", + "fertilization": "fertilisation", + "fertilize": "fertilise", + "fertilized": "fertilised", + "fertilizer": "fertiliser", + "fertilizers": "fertilisers", + "fertilizes": "fertilises", + "fertilizing": "fertilising", + "fervor": "fervour", + "fiber": "fibre", + "fiberglass": "fibreglass", + "fibers": "fibres", + "fictionalization": "fictionalisation", + "fictionalizations": "fictionalisations", + "fictionalize": "fictionalise", + "fictionalized": "fictionalised", + "fictionalizes": "fictionalises", + "fictionalizing": "fictionalising", + "filet": "fillet", + "fileted": "filleted", + "fileting": "filleting", + "filets": "fillets", + "finalization": "finalisation", + "finalize": "finalise", + "finalized": "finalised", + "finalizes": "finalises", + "finalizing": "finalising", + "flutist": "flautist", + "flutists": "flautists", + "flavor": "flavour", + "flavored": "flavoured", + "flavoring": "flavouring", + "flavorings": "flavourings", + "flavorless": "flavourless", + "flavors": "flavours", + "flavorsome": "flavoursome", + "fetal": "foetal", + "fetid": "foetid", + "fetus": "foetus", + "fetuses": "foetuses", + "formalization": "formalisation", + "formalize": "formalise", + "formalized": "formalised", + "formalizes": "formalises", + "formalizing": "formalising", + "fossilization": "fossilisation", + "fossilize": "fossilise", + "fossilized": "fossilised", + "fossilizes": "fossilises", + "fossilizing": "fossilising", + "fraternization": "fraternisation", + "fraternize": "fraternise", + "fraternized": "fraternised", + "fraternizes": "fraternises", + "fraternizing": "fraternising", + "fulfill": "fulfil", + "fulfillment": "fulfilment", + "fulfills": "fulfils", + "funneled": "funnelled", + "funneling": "funnelling", + "galvanize": "galvanise", + "galvanized": "galvanised", + "galvanizes": "galvanises", + "galvanizing": "galvanising", + "gamboled": "gambolled", + "gamboling": "gambolling", + "gasses": "gases", + "gage": "gauge", + "gaged": "gauged", + "gages": "gauges", + "gaging": "gauging", + "generalization": "generalisation", + "generalizations": "generalisations", + "generalize": "generalise", + "generalized": "generalised", + "generalizes": "generalises", + "generalizing": "generalising", + "ghettoize": "ghettoise", + "ghettoized": "ghettoised", + "ghettoizes": "ghettoises", + "ghettoizing": "ghettoising", + "gypsies": "gipsies", + "glamorize": "glamorise", + "glamorized": "glamorised", + "glamorizes": "glamorises", + "glamorizing": "glamorising", + "glamor": "glamour", + "globalization": "globalisation", + "globalize": "globalise", + "globalized": "globalised", + "globalizes": "globalises", + "globalizing": "globalising", + "gluing": "glueing", + "goiter": "goitre", + "goiters": "goitres", + "gonorrhea": "gonorrhoea", + "gram": "gramme", + "grams": "grammes", + "graveled": "gravelled", + "gray": "grey", + "grayed": "greyed", + "graying": "greying", + "grayish": "greyish", + "grayness": "greyness", + "grays": "greys", + "groveled": "grovelled", + "groveling": "grovelling", + "groin": "groyne", + "groins": "groynes", + "grueling": "gruelling", + "gruelingly": "gruellingly", + "griffin": "gryphon", + "griffins": "gryphons", + "gynecological": "gynaecological", + "gynecologist": "gynaecologist", + "gynecologists": "gynaecologists", + "gynecology": "gynaecology", + "hematological": "haematological", + "hematologist": "haematologist", + "hematologists": "haematologists", + "hematology": "haematology", + "hemoglobin": "haemoglobin", + "hemophilia": "haemophilia", + "hemophiliac": "haemophiliac", + "hemophiliacs": "haemophiliacs", + "hemorrhage": "haemorrhage", + "hemorrhaged": "haemorrhaged", + "hemorrhages": "haemorrhages", + "hemorrhaging": "haemorrhaging", + "hemorrhoids": "haemorrhoids", + "harbor": "harbour", + "harbored": "harboured", + "harboring": "harbouring", + "harbors": "harbours", + "harmonization": "harmonisation", + "harmonize": "harmonise", + "harmonized": "harmonised", + "harmonizes": "harmonises", + "harmonizing": "harmonising", + "homeopath": "homoeopath", + "homeopathic": "homoeopathic", + "homeopaths": "homoeopaths", + "homeopathy": "homoeopathy", + "homogenize": "homogenise", + "homogenized": "homogenised", + "homogenizes": "homogenises", + "homogenizing": "homogenising", + "honor": "honour", + "honorable": "honourable", + "honorably": "honourably", + "honored": "honoured", + "honoring": "honouring", + "honors": "honours", + "hospitalization": "hospitalisation", + "hospitalize": "hospitalise", + "hospitalized": "hospitalised", + "hospitalizes": "hospitalises", + "hospitalizing": "hospitalising", + "humanize": "humanise", + "humanized": "humanised", + "humanizes": "humanises", + "humanizing": "humanising", + "humor": "humour", + "humored": "humoured", + "humoring": "humouring", + "humorless": "humourless", + "humors": "humours", + "hybridize": "hybridise", + "hybridized": "hybridised", + "hybridizes": "hybridises", + "hybridizing": "hybridising", + "hypnotize": "hypnotise", + "hypnotized": "hypnotised", + "hypnotizes": "hypnotises", + "hypnotizing": "hypnotising", + "hypothesize": "hypothesise", + "hypothesized": "hypothesised", + "hypothesizes": "hypothesises", + "hypothesizing": "hypothesising", + "idealization": "idealisation", + "idealize": "idealise", + "idealized": "idealised", + "idealizes": "idealises", + "idealizing": "idealising", + "idolize": "idolise", + "idolized": "idolised", + "idolizes": "idolises", + "idolizing": "idolising", + "immobilization": "immobilisation", + "immobilize": "immobilise", + "immobilized": "immobilised", + "immobilizer": "immobiliser", + "immobilizers": "immobilisers", + "immobilizes": "immobilises", + "immobilizing": "immobilising", + "immortalize": "immortalise", + "immortalized": "immortalised", + "immortalizes": "immortalises", + "immortalizing": "immortalising", + "immunization": "immunisation", + "immunize": "immunise", + "immunized": "immunised", + "immunizes": "immunises", + "immunizing": "immunising", + "impaneled": "impanelled", + "impaneling": "impanelling", + "imperiled": "imperilled", + "imperiling": "imperilling", + "individualize": "individualise", + "individualized": "individualised", + "individualizes": "individualises", + "individualizing": "individualising", + "industrialize": "industrialise", + "industrialized": "industrialised", + "industrializes": "industrialises", + "industrializing": "industrialising", + "inflection": "inflexion", + "inflections": "inflexions", + "initialize": "initialise", + "initialized": "initialised", + "initializes": "initialises", + "initializing": "initialising", + "initialed": "initialled", + "initialing": "initialling", + "install": "instal", + "installment": "instalment", + "installments": "instalments", + "installs": "instals", + "instill": "instil", + "instills": "instils", + "institutionalization": "institutionalisation", + "institutionalize": "institutionalise", + "institutionalized": "institutionalised", + "institutionalizes": "institutionalises", + "institutionalizing": "institutionalising", + "intellectualize": "intellectualise", + "intellectualized": "intellectualised", + "intellectualizes": "intellectualises", + "intellectualizing": "intellectualising", + "internalization": "internalisation", + "internalize": "internalise", + "internalized": "internalised", + "internalizes": "internalises", + "internalizing": "internalising", + "internationalization": "internationalisation", + "internationalize": "internationalise", + "internationalized": "internationalised", + "internationalizes": "internationalises", + "internationalizing": "internationalising", + "ionization": "ionisation", + "ionize": "ionise", + "ionized": "ionised", + "ionizer": "ioniser", + "ionizers": "ionisers", + "ionizes": "ionises", + "ionizing": "ionising", + "italicize": "italicise", + "italicized": "italicised", + "italicizes": "italicises", + "italicizing": "italicising", + "itemize": "itemise", + "itemized": "itemised", + "itemizes": "itemises", + "itemizing": "itemising", + "jeopardize": "jeopardise", + "jeopardized": "jeopardised", + "jeopardizes": "jeopardises", + "jeopardizing": "jeopardising", + "jeweled": "jewelled", + "jeweler": "jeweller", + "jewelers": "jewellers", + "jewelry": "jewellery", + "judgment": "judgement", + "kilogram": "kilogramme", + "kilograms": "kilogrammes", + "kilometer": "kilometre", + "kilometers": "kilometres", + "labeled": "labelled", + "labeling": "labelling", + "labor": "labour", + "labored": "laboured", + "laborer": "labourer", + "laborers": "labourers", + "laboring": "labouring", + "labors": "labours", + "lackluster": "lacklustre", + "legalization": "legalisation", + "legalize": "legalise", + "legalized": "legalised", + "legalizes": "legalises", + "legalizing": "legalising", + "legitimize": "legitimise", + "legitimized": "legitimised", + "legitimizes": "legitimises", + "legitimizing": "legitimising", + "leukemia": "leukaemia", + "leveled": "levelled", + "leveler": "leveller", + "levelers": "levellers", + "leveling": "levelling", + "libeled": "libelled", + "libeling": "libelling", + "libelous": "libellous", + "liberalization": "liberalisation", + "liberalize": "liberalise", + "liberalized": "liberalised", + "liberalizes": "liberalises", + "liberalizing": "liberalising", + "license": "licence", + "licensed": "licenced", + "licenses": "licences", + "licensing": "licencing", + "likable": "likeable", + "lionization": "lionisation", + "lionize": "lionise", + "lionized": "lionised", + "lionizes": "lionises", + "lionizing": "lionising", + "liquidize": "liquidise", + "liquidized": "liquidised", + "liquidizer": "liquidiser", + "liquidizers": "liquidisers", + "liquidizes": "liquidises", + "liquidizing": "liquidising", + "liter": "litre", + "liters": "litres", + "localize": "localise", + "localized": "localised", + "localizes": "localises", + "localizing": "localising", + "louver": "louvre", + "louvered": "louvred", + "louvers": "louvres", + "luster": "lustre", + "magnetize": "magnetise", + "magnetized": "magnetised", + "magnetizes": "magnetises", + "magnetizing": "magnetising", + "maneuverability": "manoeuvrability", + "maneuverable": "manoeuvrable", + "maneuver": "manoeuvre", + "maneuvered": "manoeuvred", + "maneuvers": "manoeuvres", + "maneuvering": "manoeuvring", + "maneuverings": "manoeuvrings", + "marginalization": "marginalisation", + "marginalize": "marginalise", + "marginalized": "marginalised", + "marginalizes": "marginalises", + "marginalizing": "marginalising", + "marshaled": "marshalled", + "marshaling": "marshalling", + "marveled": "marvelled", + "marveling": "marvelling", + "marvelous": "marvellous", + "marvelously": "marvellously", + "materialization": "materialisation", + "materialize": "materialise", + "materialized": "materialised", + "materializes": "materialises", + "materializing": "materialising", + "maximization": "maximisation", + "maximize": "maximise", + "maximized": "maximised", + "maximizes": "maximises", + "maximizing": "maximising", + "meager": "meagre", + "mechanization": "mechanisation", + "mechanize": "mechanise", + "mechanized": "mechanised", + "mechanizes": "mechanises", + "mechanizing": "mechanising", + "medieval": "mediaeval", + "memorialize": "memorialise", + "memorialized": "memorialised", + "memorializes": "memorialises", + "memorializing": "memorialising", + "memorize": "memorise", + "memorized": "memorised", + "memorizes": "memorises", + "memorizing": "memorising", + "mesmerize": "mesmerise", + "mesmerized": "mesmerised", + "mesmerizes": "mesmerises", + "mesmerizing": "mesmerising", + "metabolize": "metabolise", + "metabolized": "metabolised", + "metabolizes": "metabolises", + "metabolizing": "metabolising", + "meter": "metre", + "meters": "metres", + "micrometer": "micrometre", + "micrometers": "micrometres", + "militarize": "militarise", + "militarized": "militarised", + "militarizes": "militarises", + "militarizing": "militarising", + "milligram": "milligramme", + "milligrams": "milligrammes", + "milliliter": "millilitre", + "milliliters": "millilitres", + "millimeter": "millimetre", + "millimeters": "millimetres", + "miniaturization": "miniaturisation", + "miniaturize": "miniaturise", + "miniaturized": "miniaturised", + "miniaturizes": "miniaturises", + "miniaturizing": "miniaturising", + "minibusses": "minibuses", + "minimize": "minimise", + "minimized": "minimised", + "minimizes": "minimises", + "minimizing": "minimising", + "misbehavior": "misbehaviour", + "misdemeanor": "misdemeanour", + "misdemeanors": "misdemeanours", + "misspelled": "misspelt", + "miter": "mitre", + "miters": "mitres", + "mobilization": "mobilisation", + "mobilize": "mobilise", + "mobilized": "mobilised", + "mobilizes": "mobilises", + "mobilizing": "mobilising", + "modeled": "modelled", + "modeler": "modeller", + "modelers": "modellers", + "modeling": "modelling", + "modernize": "modernise", + "modernized": "modernised", + "modernizes": "modernises", + "modernizing": "modernising", + "moisturize": "moisturise", + "moisturized": "moisturised", + "moisturizer": "moisturiser", + "moisturizers": "moisturisers", + "moisturizes": "moisturises", + "moisturizing": "moisturising", + "monolog": "monologue", + "monologs": "monologues", + "monopolization": "monopolisation", + "monopolize": "monopolise", + "monopolized": "monopolised", + "monopolizes": "monopolises", + "monopolizing": "monopolising", + "moralize": "moralise", + "moralized": "moralised", + "moralizes": "moralises", + "moralizing": "moralising", + "motorized": "motorised", + "mold": "mould", + "molded": "moulded", + "molder": "moulder", + "moldered": "mouldered", + "moldering": "mouldering", + "molders": "moulders", + "moldier": "mouldier", + "moldiest": "mouldiest", + "molding": "moulding", + "moldings": "mouldings", + "molds": "moulds", + "moldy": "mouldy", + "molt": "moult", + "molted": "moulted", + "molting": "moulting", + "molts": "moults", + "mustache": "moustache", + "mustached": "moustached", + "mustaches": "moustaches", + "mustachioed": "moustachioed", + "multicolored": "multicoloured", + "nationalization": "nationalisation", + "nationalizations": "nationalisations", + "nationalize": "nationalise", + "nationalized": "nationalised", + "nationalizes": "nationalises", + "nationalizing": "nationalising", + "naturalization": "naturalisation", + "naturalize": "naturalise", + "naturalized": "naturalised", + "naturalizes": "naturalises", + "naturalizing": "naturalising", + "neighbor": "neighbour", + "neighborhood": "neighbourhood", + "neighborhoods": "neighbourhoods", + "neighboring": "neighbouring", + "neighborliness": "neighbourliness", + "neighborly": "neighbourly", + "neighbors": "neighbours", + "neutralization": "neutralisation", + "neutralize": "neutralise", + "neutralized": "neutralised", + "neutralizes": "neutralises", + "neutralizing": "neutralising", + "normalization": "normalisation", + "normalize": "normalise", + "normalized": "normalised", + "normalizes": "normalises", + "normalizing": "normalising", + "odor": "odour", + "odorless": "odourless", + "odors": "odours", + "esophagus": "oesophagus", + "esophaguses": "oesophaguses", + "estrogen": "oestrogen", + "offense": "offence", + "offenses": "offences", + "omelet": "omelette", + "omelets": "omelettes", + "optimization": "optimisation", + "optimizations": "optimisations", + "optimize": "optimise", + "optimized": "optimised", + "optimizes": "optimises", + "optimizing": "optimising", + "organization": "organisation", + "organizational": "organisational", + "organizations": "organisations", + "organize": "organise", + "organized": "organised", + "organizer": "organiser", + "organizers": "organisers", + "organizes": "organises", + "organizing": "organising", + "orthopedic": "orthopaedic", + "orthopedics": "orthopaedics", + "ostracize": "ostracise", + "ostracized": "ostracised", + "ostracizes": "ostracises", + "ostracizing": "ostracising", + "outmaneuver": "outmanoeuvre", + "outmaneuvered": "outmanoeuvred", + "outmaneuvers": "outmanoeuvres", + "outmaneuvering": "outmanoeuvring", + "overemphasize": "overemphasise", + "overemphasized": "overemphasised", + "overemphasizes": "overemphasises", + "overemphasizing": "overemphasising", + "oxidization": "oxidisation", + "oxidize": "oxidise", + "oxidized": "oxidised", + "oxidizes": "oxidises", + "oxidizing": "oxidising", + "pederast": "paederast", + "pederasts": "paederasts", + "pediatric": "paediatric", + "pediatrician": "paediatrician", + "pediatricians": "paediatricians", + "pediatrics": "paediatrics", + "pedophile": "paedophile", + "pedophiles": "paedophiles", + "pedophilia": "paedophilia", + "paleolithic": "palaeolithic", + "paleontologist": "palaeontologist", + "paleontologists": "palaeontologists", + "paleontology": "palaeontology", + "paneled": "panelled", + "paneling": "panelling", + "panelist": "panellist", + "panelists": "panellists", + "paralyze": "paralyse", + "paralyzed": "paralysed", + "paralyzes": "paralyses", + "paralyzing": "paralysing", + "parceled": "parcelled", + "parceling": "parcelling", + "parlor": "parlour", + "parlors": "parlours", + "particularize": "particularise", + "particularized": "particularised", + "particularizes": "particularises", + "particularizing": "particularising", + "passivization": "passivisation", + "passivize": "passivise", + "passivized": "passivised", + "passivizes": "passivises", + "passivizing": "passivising", + "pasteurization": "pasteurisation", + "pasteurize": "pasteurise", + "pasteurized": "pasteurised", + "pasteurizes": "pasteurises", + "pasteurizing": "pasteurising", + "patronize": "patronise", + "patronized": "patronised", + "patronizes": "patronises", + "patronizing": "patronising", + "patronizingly": "patronisingly", + "pedaled": "pedalled", + "pedaling": "pedalling", + "pedestrianization": "pedestrianisation", + "pedestrianize": "pedestrianise", + "pedestrianized": "pedestrianised", + "pedestrianizes": "pedestrianises", + "pedestrianizing": "pedestrianising", + "penalize": "penalise", + "penalized": "penalised", + "penalizes": "penalises", + "penalizing": "penalising", + "penciled": "pencilled", + "penciling": "pencilling", + "personalize": "personalise", + "personalized": "personalised", + "personalizes": "personalises", + "personalizing": "personalising", + "pharmacopeia": "pharmacopoeia", + "pharmacopeias": "pharmacopoeias", + "philosophize": "philosophise", + "philosophized": "philosophised", + "philosophizes": "philosophises", + "philosophizing": "philosophising", + "filter": "philtre", + "filters": "philtres", + "phony": "phoney", + "plagiarize": "plagiarise", + "plagiarized": "plagiarised", + "plagiarizes": "plagiarises", + "plagiarizing": "plagiarising", + "plow": "plough", + "plowed": "ploughed", + "plowing": "ploughing", + "plowman": "ploughman", + "plowmen": "ploughmen", + "plows": "ploughs", + "plowshare": "ploughshare", + "plowshares": "ploughshares", + "polarization": "polarisation", + "polarize": "polarise", + "polarized": "polarised", + "polarizes": "polarises", + "polarizing": "polarising", + "politicization": "politicisation", + "politicize": "politicise", + "politicized": "politicised", + "politicizes": "politicises", + "politicizing": "politicising", + "popularization": "popularisation", + "popularize": "popularise", + "popularized": "popularised", + "popularizes": "popularises", + "popularizing": "popularising", + "pouf": "pouffe", + "poufs": "pouffes", + "practice": "practise", + "practiced": "practised", + "practices": "practises", + "practicing": "practising", + "presidium": "praesidium", + "presidiums": "praesidiums", + "pressurization": "pressurisation", + "pressurize": "pressurise", + "pressurized": "pressurised", + "pressurizes": "pressurises", + "pressurizing": "pressurising", + "pretense": "pretence", + "pretenses": "pretences", + "primeval": "primaeval", + "prioritization": "prioritisation", + "prioritize": "prioritise", + "prioritized": "prioritised", + "prioritizes": "prioritises", + "prioritizing": "prioritising", + "privatization": "privatisation", + "privatizations": "privatisations", + "privatize": "privatise", + "privatized": "privatised", + "privatizes": "privatises", + "privatizing": "privatising", + "professionalization": "professionalisation", + "professionalize": "professionalise", + "professionalized": "professionalised", + "professionalizes": "professionalises", + "professionalizing": "professionalising", + "program": "programme", + "programs": "programmes", + "prolog": "prologue", + "prologs": "prologues", + "propagandize": "propagandise", + "propagandized": "propagandised", + "propagandizes": "propagandises", + "propagandizing": "propagandising", + "proselytize": "proselytise", + "proselytized": "proselytised", + "proselytizer": "proselytiser", + "proselytizers": "proselytisers", + "proselytizes": "proselytises", + "proselytizing": "proselytising", + "psychoanalyze": "psychoanalyse", + "psychoanalyzed": "psychoanalysed", + "psychoanalyzes": "psychoanalyses", + "psychoanalyzing": "psychoanalysing", + "publicize": "publicise", + "publicized": "publicised", + "publicizes": "publicises", + "publicizing": "publicising", + "pulverization": "pulverisation", + "pulverize": "pulverise", + "pulverized": "pulverised", + "pulverizes": "pulverises", + "pulverizing": "pulverising", + "pummel": "pummelled", + "pummeled": "pummelling", + "pajama": "pyjama", + "pajamas": "pyjamas", + "pizzazz": "pzazz", + "quarreled": "quarrelled", + "quarreling": "quarrelling", + "radicalize": "radicalise", + "radicalized": "radicalised", + "radicalizes": "radicalises", + "radicalizing": "radicalising", + "rancor": "rancour", + "randomize": "randomise", + "randomized": "randomised", + "randomizes": "randomises", + "randomizing": "randomising", + "rationalization": "rationalisation", + "rationalizations": "rationalisations", + "rationalize": "rationalise", + "rationalized": "rationalised", + "rationalizes": "rationalises", + "rationalizing": "rationalising", + "raveled": "ravelled", + "raveling": "ravelling", + "realizable": "realisable", + "realization": "realisation", + "realizations": "realisations", + "realize": "realise", + "realized": "realised", + "realizes": "realises", + "realizing": "realising", + "recognizable": "recognisable", + "recognizably": "recognisably", + "recognizance": "recognisance", + "recognize": "recognise", + "recognized": "recognised", + "recognizes": "recognises", + "recognizing": "recognising", + "reconnoiter": "reconnoitre", + "reconnoitered": "reconnoitred", + "reconnoiters": "reconnoitres", + "reconnoitering": "reconnoitring", + "refueled": "refuelled", + "refueling": "refuelling", + "regularization": "regularisation", + "regularize": "regularise", + "regularized": "regularised", + "regularizes": "regularises", + "regularizing": "regularising", + "remodeled": "remodelled", + "remodeling": "remodelling", + "remold": "remould", + "remolded": "remoulded", + "remolding": "remoulding", + "remolds": "remoulds", + "reorganization": "reorganisation", + "reorganizations": "reorganisations", + "reorganize": "reorganise", + "reorganized": "reorganised", + "reorganizes": "reorganises", + "reorganizing": "reorganising", + "reveled": "revelled", + "reveler": "reveller", + "revelers": "revellers", + "reveling": "revelling", + "revitalize": "revitalise", + "revitalized": "revitalised", + "revitalizes": "revitalises", + "revitalizing": "revitalising", + "revolutionize": "revolutionise", + "revolutionized": "revolutionised", + "revolutionizes": "revolutionises", + "revolutionizing": "revolutionising", + "rhapsodize": "rhapsodise", + "rhapsodized": "rhapsodised", + "rhapsodizes": "rhapsodises", + "rhapsodizing": "rhapsodising", + "rigor": "rigour", + "rigors": "rigours", + "ritualized": "ritualised", + "rivaled": "rivalled", + "rivaling": "rivalling", + "romanticize": "romanticise", + "romanticized": "romanticised", + "romanticizes": "romanticises", + "romanticizing": "romanticising", + "rumor": "rumour", + "rumored": "rumoured", + "rumors": "rumours", + "saber": "sabre", + "sabers": "sabres", + "saltpeter": "saltpetre", + "sanitize": "sanitise", + "sanitized": "sanitised", + "sanitizes": "sanitises", + "sanitizing": "sanitising", + "satirize": "satirise", + "satirized": "satirised", + "satirizes": "satirises", + "satirizing": "satirising", + "savior": "saviour", + "saviors": "saviours", + "savor": "savour", + "savored": "savoured", + "savories": "savouries", + "savoring": "savouring", + "savors": "savours", + "savory": "savoury", + "scandalize": "scandalise", + "scandalized": "scandalised", + "scandalizes": "scandalises", + "scandalizing": "scandalising", + "skeptic": "sceptic", + "skeptical": "sceptical", + "skeptically": "sceptically", + "skepticism": "scepticism", + "skeptics": "sceptics", + "scepter": "sceptre", + "scepters": "sceptres", + "scrutinize": "scrutinise", + "scrutinized": "scrutinised", + "scrutinizes": "scrutinises", + "scrutinizing": "scrutinising", + "secularization": "secularisation", + "secularize": "secularise", + "secularized": "secularised", + "secularizes": "secularises", + "secularizing": "secularising", + "sensationalize": "sensationalise", + "sensationalized": "sensationalised", + "sensationalizes": "sensationalises", + "sensationalizing": "sensationalising", + "sensitize": "sensitise", + "sensitized": "sensitised", + "sensitizes": "sensitises", + "sensitizing": "sensitising", + "sentimentalize": "sentimentalise", + "sentimentalized": "sentimentalised", + "sentimentalizes": "sentimentalises", + "sentimentalizing": "sentimentalising", + "sepulcher": "sepulchre", + "sepulchers": "sepulchres", + "serialization": "serialisation", + "serializations": "serialisations", + "serialize": "serialise", + "serialized": "serialised", + "serializes": "serialises", + "serializing": "serialising", + "sermonize": "sermonise", + "sermonized": "sermonised", + "sermonizes": "sermonises", + "sermonizing": "sermonising", + "sheik": "sheikh", + "shoveled": "shovelled", + "shoveling": "shovelling", + "shriveled": "shrivelled", + "shriveling": "shrivelling", + "signalize": "signalise", + "signalized": "signalised", + "signalizes": "signalises", + "signalizing": "signalising", + "signaled": "signalled", + "signaling": "signalling", + "smolder": "smoulder", + "smoldered": "smouldered", + "smoldering": "smouldering", + "smolders": "smoulders", + "sniveled": "snivelled", + "sniveling": "snivelling", + "snorkeled": "snorkelled", + "snorkeling": "snorkelling", + "snowplow": "snowploughs", + "socialization": "socialisation", + "socialize": "socialise", + "socialized": "socialised", + "socializes": "socialises", + "socializing": "socialising", + "sodomize": "sodomise", + "sodomized": "sodomised", + "sodomizes": "sodomises", + "sodomizing": "sodomising", + "solemnize": "solemnise", + "solemnized": "solemnised", + "solemnizes": "solemnises", + "solemnizing": "solemnising", + "somber": "sombre", + "specialization": "specialisation", + "specializations": "specialisations", + "specialize": "specialise", + "specialized": "specialised", + "specializes": "specialises", + "specializing": "specialising", + "specter": "spectre", + "specters": "spectres", + "spiraled": "spiralled", + "spiraling": "spiralling", + "splendor": "splendour", + "splendors": "splendours", + "squirreled": "squirrelled", + "squirreling": "squirrelling", + "stabilization": "stabilisation", + "stabilize": "stabilise", + "stabilized": "stabilised", + "stabilizer": "stabiliser", + "stabilizers": "stabilisers", + "stabilizes": "stabilises", + "stabilizing": "stabilising", + "standardization": "standardisation", + "standardize": "standardise", + "standardized": "standardised", + "standardizes": "standardises", + "standardizing": "standardising", + "stenciled": "stencilled", + "stenciling": "stencilling", + "sterilization": "sterilisation", + "sterilizations": "sterilisations", + "sterilize": "sterilise", + "sterilized": "sterilised", + "sterilizer": "steriliser", + "sterilizers": "sterilisers", + "sterilizes": "sterilises", + "sterilizing": "sterilising", + "stigmatization": "stigmatisation", + "stigmatize": "stigmatise", + "stigmatized": "stigmatised", + "stigmatizes": "stigmatises", + "stigmatizing": "stigmatising", + "subsidization": "subsidisation", + "subsidize": "subsidise", + "subsidized": "subsidised", + "subsidizer": "subsidiser", + "subsidizers": "subsidisers", + "subsidizes": "subsidises", + "subsidizing": "subsidising", + "succor": "succour", + "succored": "succoured", + "succoring": "succouring", + "succors": "succours", + "sulfate": "sulphate", + "sulfates": "sulphates", + "sulfide": "sulphide", + "sulfides": "sulphides", + "sulfur": "sulphur", + "sulfurous": "sulphurous", + "summarize": "summarise", + "summarized": "summarised", + "summarizes": "summarises", + "summarizing": "summarising", + "swiveled": "swivelled", + "swiveling": "swivelling", + "symbolize": "symbolise", + "symbolized": "symbolised", + "symbolizes": "symbolises", + "symbolizing": "symbolising", + "sympathize": "sympathise", + "sympathized": "sympathised", + "sympathizer": "sympathiser", + "sympathizers": "sympathisers", + "sympathizes": "sympathises", + "sympathizing": "sympathising", + "synchronization": "synchronisation", + "synchronize": "synchronise", + "synchronized": "synchronised", + "synchronizes": "synchronises", + "synchronizing": "synchronising", + "synthesize": "synthesise", + "synthesized": "synthesised", + "synthesizer": "synthesiser", + "synthesizers": "synthesisers", + "synthesizes": "synthesises", + "synthesizing": "synthesising", + "siphon": "syphon", + "siphoned": "syphoned", + "siphoning": "syphoning", + "siphons": "syphons", + "systematization": "systematisation", + "systematize": "systematise", + "systematized": "systematised", + "systematizes": "systematises", + "systematizing": "systematising", + "tantalize": "tantalise", + "tantalized": "tantalised", + "tantalizes": "tantalises", + "tantalizing": "tantalising", + "tantalizingly": "tantalisingly", + "tasseled": "tasselled", + "technicolor": "technicolour", + "temporize": "temporise", + "temporized": "temporised", + "temporizes": "temporises", + "temporizing": "temporising", + "tenderize": "tenderise", + "tenderized": "tenderised", + "tenderizes": "tenderises", + "tenderizing": "tenderising", + "terrorize": "terrorise", + "terrorized": "terrorised", + "terrorizes": "terrorises", + "terrorizing": "terrorising", + "theater": "theatre", + "theatergoer": "theatregoer", + "theatergoers": "theatregoers", + "theaters": "theatres", + "theorize": "theorise", + "theorized": "theorised", + "theorizes": "theorises", + "theorizing": "theorising", + "ton": "tonne", + "tons": "tonnes", + "toweled": "towelled", + "toweling": "towelling", + "toxemia": "toxaemia", + "tranquilize": "tranquillize", + "tranquilized": "tranquillized", + "tranquilizer": "tranquillizer", + "tranquilizers": "tranquillizers", + "tranquilizes": "tranquillizes", + "tranquilizing": "tranquillizing", + "tranquility": "tranquilly", + "transistorized": "transistorised", + "traumatize": "traumatise", + "traumatized": "traumatised", + "traumatizes": "traumatises", + "traumatizing": "traumatising", + "traveled": "travelled", + "traveler": "traveller", + "travelers": "travellers", + "traveling": "travelling", + "travelog": "travelogue", + "travelogs": "travelogues", + "trialed": "trialled", + "trialing": "trialling", + "tricolor": "tricolour", + "tricolors": "tricolours", + "trivialize": "trivialise", + "trivialized": "trivialised", + "trivializes": "trivialises", + "trivializing": "trivialising", + "tumor": "tumour", + "tumors": "tumours", + "tunneled": "tunnelled", + "tunneling": "tunnelling", + "tyrannize": "tyrannise", + "tyrannized": "tyrannised", + "tyrannizes": "tyrannises", + "tyrannizing": "tyrannising", + "tire": "tyre", + "tires": "tyres", + "unauthorized": "unauthorised", + "uncivilized": "uncivilised", + "underutilized": "underutilised", + "unequaled": "unequalled", + "unfavorable": "unfavourable", + "unfavorably": "unfavourably", + "unionization": "unionisation", + "unionize": "unionise", + "unionized": "unionised", + "unionizes": "unionises", + "unionizing": "unionising", + "unorganized": "unorganised", + "unraveled": "unravelled", + "unraveling": "unravelling", + "unrecognizable": "unrecognisable", + "unrecognized": "unrecognised", + "unrivaled": "unrivalled", + "unsavory": "unsavoury", + "untrammeled": "untrammelled", + "urbanization": "urbanisation", + "urbanize": "urbanise", + "urbanized": "urbanised", + "urbanizes": "urbanises", + "urbanizing": "urbanising", + "utilizable": "utilisable", + "utilization": "utilisation", + "utilize": "utilise", + "utilized": "utilised", + "utilizes": "utilises", + "utilizing": "utilising", + "valor": "valour", + "vandalize": "vandalise", + "vandalized": "vandalised", + "vandalizes": "vandalises", + "vandalizing": "vandalising", + "vaporization": "vaporisation", + "vaporize": "vaporise", + "vaporized": "vaporised", + "vaporizes": "vaporises", + "vaporizing": "vaporising", + "vapor": "vapour", + "vapors": "vapours", + "verbalize": "verbalise", + "verbalized": "verbalised", + "verbalizes": "verbalises", + "verbalizing": "verbalising", + "victimization": "victimisation", + "victimize": "victimise", + "victimized": "victimised", + "victimizes": "victimises", + "victimizing": "victimising", + "videodisk": "videodisc", + "videodisks": "videodiscs", + "vigor": "vigour", + "visualization": "visualisation", + "visualizations": "visualisations", + "visualize": "visualise", + "visualized": "visualised", + "visualizes": "visualises", + "visualizing": "visualising", + "vocalization": "vocalisation", + "vocalizations": "vocalisations", + "vocalize": "vocalise", + "vocalized": "vocalised", + "vocalizes": "vocalises", + "vocalizing": "vocalising", + "vulcanized": "vulcanised", + "vulgarization": "vulgarisation", + "vulgarize": "vulgarise", + "vulgarized": "vulgarised", + "vulgarizes": "vulgarises", + "vulgarizing": "vulgarising", + "watercolor": "watercolour", + "watercolors": "watercolours", + "weaseled": "weaselled", + "weaseling": "weaselling", + "westernization": "westernisation", + "westernize": "westernise", + "westernized": "westernised", + "westernizes": "westernises", + "westernizing": "westernising", + "womanize": "womanise", + "womanized": "womanised", + "womanizer": "womaniser", + "womanizers": "womanisers", + "womanizes": "womanises", + "womanizing": "womanising", + "woolen": "woollen", + "woolens": "woollens", + "woolies": "woollies", + "wooly": "woolly", + "worshiped": "worshipped", + "worshiping": "worshipping", + "worshiper": "worshipper", + "yodeled": "yodelled", + "yodeling": "yodelling", + "yogurt": "yoghurt", + "yogurts": "yoghurts", +} diff --git a/good_ai/src/sus/data/punctuations.py b/good_ai/src/sus/data/punctuations.py new file mode 100644 index 0000000..51d76ec --- /dev/null +++ b/good_ai/src/sus/data/punctuations.py @@ -0,0 +1,22 @@ +sentence_ending_punctuations = [".", "?", "!", ":", ";"] + +# punctuations that usually have no space between them and the character to their left +left_regular_punctuations = [ + *sentence_ending_punctuations, + ",", + "'", + "%", + ")", + "}", + "]", +] + +# punctuations that usually have no space between them and the character to their right +right_regular_punctuations = [ + "(", + "{", + "[", + "$", + "€", + "#", +] diff --git a/good_ai/src/sus/evaluate_ranking/__init__.py b/good_ai/src/sus/evaluate_ranking/__init__.py new file mode 100644 index 0000000..94226bb --- /dev/null +++ b/good_ai/src/sus/evaluate_ranking/__init__.py @@ -0,0 +1,2 @@ +from .draw_f1_iso_lines import draw_f1_iso_lines +from .evaluate_ranking import evaluate_ranking diff --git a/good_ai/src/sus/evaluate_ranking/draw_f1_iso_lines.py b/good_ai/src/sus/evaluate_ranking/draw_f1_iso_lines.py new file mode 100644 index 0000000..d54724b --- /dev/null +++ b/good_ai/src/sus/evaluate_ranking/draw_f1_iso_lines.py @@ -0,0 +1,28 @@ +from typing import Optional + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.axes import Axes + + +def draw_f1_iso_lines( + resolution: int = 1000, + min: float = 0.2, + max: float = 0.8, + steps: int = 4, + axes: Optional[Axes] = None, +) -> None: + if axes is None: + axes = plt.axes() + + for f_score in np.linspace(min, max, num=steps): + x = np.linspace(f_score / (2 - f_score), 1, num=resolution) + y = f_score * x / (2 * x - f_score) + + axes.plot(x[y >= 0], y[y >= 0], color="gray", alpha=0.2) + + axes.annotate( + f"f1={f_score:0.1f}", + backgroundcolor="w", + xy=(0.9, y[int(resolution * 0.9)] + 0.02), + ) diff --git a/good_ai/src/sus/evaluate_ranking/evaluate_ranking.py b/good_ai/src/sus/evaluate_ranking/evaluate_ranking.py new file mode 100644 index 0000000..78d9a80 --- /dev/null +++ b/good_ai/src/sus/evaluate_ranking/evaluate_ranking.py @@ -0,0 +1,90 @@ +from pathlib import Path +from typing import Dict, List, Optional, Union + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +from sklearn.metrics import average_precision_score, precision_recall_curve + +from ..unique import unique +from .draw_f1_iso_lines import draw_f1_iso_lines + + +def evaluate_ranking( + expected: List[Union[str, float]], + actual_scores: List[float], + target_recall: float, + title: Optional[str] = "", + disable_interpolation: bool = False, + axes: Optional[plt.Axes] = None, + output_svg: Optional[Path] = None, + reverse_order: bool = False, + plot: bool = True, +) -> Dict[Union[str, float], float]: + assert 0 <= target_recall <= 1 + + if plot and axes is None: + fig = plt.figure(figsize=(20, 10)) + fig.patch.set_facecolor("white") + ax = plt.axes() + else: + ax = axes + + classes = sorted(unique(expected), reverse=reverse_order) + str_classes = [str(c) for c in classes] + + with matplotlib.rc_context({"font.size": 20}): + if plot: + ax.set_xmargin(0) + + draw_f1_iso_lines(axes=ax) + + results: Dict[Union[str, float], float] = {} + for i in range(len(classes) - 1): + binarized_expected = [ + (v < classes[i]) if reverse_order else (v > classes[i]) + for v in expected + ] + precision, recall, _ = precision_recall_curve( + binarized_expected, actual_scores + ) + + if not disable_interpolation: + for j in range(1, len(precision)): + precision[j] = max(precision[j - 1], precision[j]) + + closest_recall_index = np.argmin(np.abs(recall - target_recall)) + precision_at_closest_recall = precision[closest_recall_index] + average_precision = average_precision_score( + binarized_expected, actual_scores + ) + results[classes[i]] = precision_at_closest_recall + + if plot: + ax.plot( + recall, + precision, + label=f"{'|'.join(str_classes[:i + 1])} ↔ {'|'.join(str_classes[i+1:])} (P@{target_recall:.2f}={precision_at_closest_recall:.2f}, AP={average_precision:.2f})", + ) + + if plot: + ax.legend(loc="upper right") + ax.axvline(x=target_recall, linestyle="--", color="#55c6bb", linewidth=2.0) + + if title is None: + title = "Ranking evaluation" + + ax.set_title(f'{title} ({" < ".join(str_classes)})', pad=20) + + ax.set_xlabel("Recall") + ax.set_ylabel("Precision") + + ax.set_xticks([target_recall] + sorted(ax.get_xticks())) + + if plot and output_svg is None: + if axes is None: + plt.show() + elif output_svg: + plt.savefig(output_svg, format="svg") + + return results diff --git a/good_ai/src/sus/external/__init__.py b/good_ai/src/sus/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/good_ai/src/sus/external/negspacy/README.md b/good_ai/src/sus/external/negspacy/README.md new file mode 100644 index 0000000..213da8d --- /dev/null +++ b/good_ai/src/sus/external/negspacy/README.md @@ -0,0 +1 @@ +https://github.com/jenojp/negspacy diff --git a/good_ai/src/sus/external/negspacy/__init__.py b/good_ai/src/sus/external/negspacy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/good_ai/src/sus/external/negspacy/negation.py b/good_ai/src/sus/external/negspacy/negation.py new file mode 100644 index 0000000..ff9115a --- /dev/null +++ b/good_ai/src/sus/external/negspacy/negation.py @@ -0,0 +1,222 @@ +import logging + +from spacy.language import Language +from spacy.matcher import PhraseMatcher +from spacy.tokens import Token + +from .termsets import termset + +default_ts = termset("en").get_patterns() + + +@Language.factory( + "negex", + default_config={ + "neg_termset": default_ts, + "extension_name": "negex", + "chunk_prefix": list(), + }, +) +class Negex: + """ + A spaCy pipeline component which identifies negated tokens in text. + + Based on: NegEx - A Simple Algorithm for Identifying Negated Findings and Diseasesin Discharge Summaries + Chapman, Bridewell, Hanbury, Cooper, Buchanan + + Parameters + ---------- + nlp: object + spaCy language object + termset_lang: str + language code, if using default termsets (e.g. "en" for english) + extension_name: str + defaults to "negex"; whether entity is negated is then available as ent._.negex + pseudo_negations: list + list of phrases that cancel out a negation, if empty, defaults are used + preceding_negations: list + negations that appear before an entity, if empty, defaults are used + following_negations: list + negations that appear after an entity, if empty, defaults are used + termination: list + phrases that "terminate" a sentence for processing purposes such as "but". If empty, defaults are used + + """ + + def __init__( + self, + nlp: Language, + name: str, + neg_termset: dict, + extension_name: str, + chunk_prefix: list, + ): + if not Token.has_extension(extension_name): + Token.set_extension(extension_name, default=False, force=True) + + ts = neg_termset + expected_keys = [ + "pseudo_negations", + "preceding_negations", + "following_negations", + "termination", + ] + if not set(ts.keys()) == set(expected_keys): + raise KeyError( + f"Unexpected or missing keys in 'neg_termset', expected: {expected_keys}, instead got: {list(ts.keys())}" + ) + + self.pseudo_negations = ts["pseudo_negations"] + self.preceding_negations = ts["preceding_negations"] + self.following_negations = ts["following_negations"] + self.termination = ts["termination"] + + self.nlp = nlp + self.extension_name = extension_name + self.build_patterns() + self.chunk_prefix = list(nlp.tokenizer.pipe(chunk_prefix)) + + def build_patterns(self): + # efficiently build spaCy matcher patterns + self.matcher = PhraseMatcher(self.nlp.vocab, attr="LOWER") + + self.pseudo_patterns = list(self.nlp.tokenizer.pipe(self.pseudo_negations)) + self.matcher.add("pseudo", None, *self.pseudo_patterns) + + self.preceding_patterns = list( + self.nlp.tokenizer.pipe(self.preceding_negations) + ) + self.matcher.add("Preceding", None, *self.preceding_patterns) + + self.following_patterns = list( + self.nlp.tokenizer.pipe(self.following_negations) + ) + self.matcher.add("Following", None, *self.following_patterns) + + self.termination_patterns = list(self.nlp.tokenizer.pipe(self.termination)) + self.matcher.add("Termination", None, *self.termination_patterns) + + def process_negations(self, doc): + """ + Find negations in doc and clean candidate negations to remove pseudo negations + + Parameters + ---------- + doc: object + spaCy Doc object + + Returns + ------- + preceding: list + list of tuples for preceding negations + following: list + list of tuples for following negations + terminating: list + list of tuples of terminating phrases + + """ + ### + # does not work properly in spacy 2.1.8. Will incorporate after 2.2. + # Relying on user to use NER in meantime + # see https://github.com/jenojp/negspacy/issues/7 + ### + # if not doc.is_nered: + # raise ValueError( + # "Negations are evaluated for Named Entities found in text. " + # "Your SpaCy pipeline does not included Named Entity resolution. " + # "Please ensure it is enabled or choose a different language model that includes it." + # ) + preceding = list() + following = list() + terminating = list() + + matches = self.matcher(doc) + pseudo = [ + (match_id, start, end) + for match_id, start, end in matches + if self.nlp.vocab.strings[match_id] == "pseudo" + ] + + for match_id, start, end in matches: + if self.nlp.vocab.strings[match_id] == "pseudo": + continue + pseudo_flag = False + for p in pseudo: + if start >= p[1] and start <= p[2]: + pseudo_flag = True + continue + if not pseudo_flag: + if self.nlp.vocab.strings[match_id] == "Preceding": + preceding.append((match_id, start, end)) + elif self.nlp.vocab.strings[match_id] == "Following": + following.append((match_id, start, end)) + elif self.nlp.vocab.strings[match_id] == "Termination": + terminating.append((match_id, start, end)) + else: + logging.warnings( + f"phrase {doc[start:end].text} not in one of the expected matcher types." + ) + return preceding, following, terminating + + def termination_boundaries(self, doc, terminating): + """ + Create sub sentences based on terminations found in text. + + Parameters + ---------- + doc: object + spaCy Doc object + terminating: list + list of tuples with (match_id, start, end) + + returns + ------- + boundaries: list + list of tuples with (start, end) of spans + + """ + sent_starts = [sent.start for sent in doc.sents] + terminating_starts = [t[1] for t in terminating] + starts = sent_starts + terminating_starts + [len(doc)] + starts.sort() + boundaries = list() + index = 0 + for i, start in enumerate(starts): + if not i == 0: + boundaries.append((index, start)) + index = start + return boundaries + + def negex(self, doc): + """ + Negates entities of interest + + Parameters + ---------- + doc: object + spaCy Doc object + + """ + preceding, following, terminating = self.process_negations(doc) + boundaries = self.termination_boundaries(doc, terminating) + for b in boundaries: + sub_preceding = [i for i in preceding if b[0] <= i[1] < b[1]] + sub_following = [i for i in following if b[0] <= i[1] < b[1]] + + for e in doc[b[0] : b[1]]: + if any(pre < e.i for pre in [i[1] for i in sub_preceding]): + e._.set(self.extension_name, True) + continue + if any(fol > e.i for fol in [i[2] for i in sub_following]): + e._.set(self.extension_name, True) + continue + if self.chunk_prefix: + if any( + e.text.lower().startswith(c.text.lower()) + for c in self.chunk_prefix + ): + e._.set(self.extension_name, True) + return doc + + def __call__(self, doc): + return self.negex(doc) diff --git a/good_ai/src/sus/external/negspacy/termsets.py b/good_ai/src/sus/external/negspacy/termsets.py new file mode 100644 index 0000000..a226aba --- /dev/null +++ b/good_ai/src/sus/external/negspacy/termsets.py @@ -0,0 +1,229 @@ +""" +Default termsets for various languages +""" + +LANGUAGES = dict() + +# english termset dictionary +en = dict() +pseudo = [ + "no further", + "not able to be", + "not certain if", + "not certain whether", + "not necessarily", + "without any further", + "without difficulty", + "without further", + "might not", + "not only", + "no increase", + "no significant change", + "no change", + "no definite change", + "not extend", + "not cause", +] +en["pseudo_negations"] = pseudo + +preceding = [ + "absence of", + "declined", + "denied", + "denies", + "denying", + "no sign of", + "no signs of", + "not", + "not demonstrate", + "symptoms atypical", + "doubt", + "negative for", + "no", + "versus", + "without", + "doesn't", + "doesnt", + "don't", + "dont", + "didn't", + "didnt", + "wasn't", + "wasnt", + "weren't", + "werent", + "isn't", + "isnt", + "aren't", + "arent", + "cannot", + "can't", + "cant", + "couldn't", + "couldnt", + "never", +] +en["preceding_negations"] = preceding + +following = [ + "declined", + "unlikely", + "was not", + "were not", + "wasn't", + "wasnt", + "weren't", + "werent", +] +en["following_negations"] = following + +termination = [ + "although", + "apart from", + "as there are", + "aside from", + "but", + "except", + "however", + "involving", + "nevertheless", + "still", + "though", + "which", + "yet", +] +en["termination"] = termination + +LANGUAGES["en"] = en + +# en_clinical builds upon en +en_clinical = dict() +pseudo_clinical = pseudo + [ + "gram negative", + "not rule out", + "not ruled out", + "not been ruled out", + "not drain", + "no suspicious change", + "no interval change", + "no significant interval change", +] +en_clinical["pseudo_negations"] = pseudo_clinical + +preceding_clinical = preceding + [ + "patient was not", + "without indication of", + "without sign of", + "without signs of", + "without any reactions or signs of", + "no complaints of", + "no evidence of", + "no cause of", + "evaluate for", + "fails to reveal", + "free of", + "never developed", + "never had", + "did not exhibit", + "rules out", + "rule out", + "rule him out", + "rule her out", + "rule patient out", + "rule the patient out", + "ruled out", + "ruled him out", + "ruled her out", + "ruled patient out", + "ruled the patient out", + "r/o", + "ro", +] +en_clinical["preceding_negations"] = preceding_clinical + +following_clinical = following + ["was ruled out", "were ruled out", "free"] +en_clinical["following_negations"] = following_clinical + +termination_clinical = termination + [ + "cause for", + "cause of", + "causes for", + "causes of", + "etiology for", + "etiology of", + "origin for", + "origin of", + "origins for", + "origins of", + "other possibilities of", + "reason for", + "reason of", + "reasons for", + "reasons of", + "secondary to", + "source for", + "source of", + "sources for", + "sources of", + "trigger event for", +] +en_clinical["termination"] = termination_clinical +LANGUAGES["en_clinical"] = en_clinical + +en_clinical_sensitive = dict() + +preceding_clinical_sensitive = preceding_clinical + [ + "concern for", + "supposed", + "which causes", + "leads to", + "h/o", + "history of", + "instead of", + "if you experience", + "if you get", + "teaching the patient", + "taught the patient", + "teach the patient", + "educated the patient", + "educate the patient", + "educating the patient", + "monitored for", + "monitor for", + "test for", + "tested for", +] +en_clinical_sensitive["pseudo_negations"] = pseudo_clinical +en_clinical_sensitive["preceding_negations"] = preceding_clinical_sensitive +en_clinical_sensitive["following_negations"] = following_clinical +en_clinical_sensitive["termination"] = termination_clinical + +LANGUAGES["en_clinical_sensitive"] = en_clinical_sensitive + + +class termset: + def __init__(self, termset_lang): + self.pattern_types = [ + "pseudo_negations", + "preceding_negations", + "following_negations", + "termination", + ] + self.terms = LANGUAGES[termset_lang] + + def get_patterns(self): + return self.terms + + def remove_patterns(self, pattern_dict): + for key, value in pattern_dict.items(): + if key in self.pattern_types: + self.terms[key] = [i for i in self.terms[key] if i not in value] + else: + raise ValueError(f"Unexpected key: {key} not in {self.pattern_types}") + + def add_patterns(self, pattern_dict): + for key, value in pattern_dict.items(): + if key in self.pattern_types: + self.terms[key] = list(set(self.terms[key] + value)) + else: + raise ValueError(f"Unexpected key: {key} not in {self.pattern_types}") diff --git a/good_ai/src/sus/external/pylatexenc/README.md b/good_ai/src/sus/external/pylatexenc/README.md new file mode 100644 index 0000000..4190174 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/README.md @@ -0,0 +1 @@ +https://github.com/phfaist/pylatexenc diff --git a/good_ai/src/sus/external/pylatexenc/__init__.py b/good_ai/src/sus/external/pylatexenc/__init__.py new file mode 100644 index 0000000..6a9e19a --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/__init__.py @@ -0,0 +1,37 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2015 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +""" +Utilities for LaTeX to/from Unicode Text conversion. + +Main Site: + + https://github.com/phfaist/pylatexenc/ + +""" + +from .version import version_str as _version_str + +__version__ = _version_str diff --git a/good_ai/src/sus/external/pylatexenc/_util.py b/good_ai/src/sus/external/pylatexenc/_util.py new file mode 100644 index 0000000..65f7617 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/_util.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2019 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +# Internal module. Internal API may move, disappear or otherwise change at any +# time and without notice. + + +try: + # Python >= 3.3 + from collections.abc import MutableMapping +except ImportError: + from collections import MutableMapping + +import bisect +import warnings + +# ------------------------------------------------------------------------------ + + +def pylatexenc_deprecated_ver(ver, msg, stacklevel=2): + warnings.warn( + "Deprecated (pylatexenc {}): {} ".format(ver, msg.strip()), + DeprecationWarning, + stacklevel=stacklevel + 1, + ) + + +def pylatexenc_deprecated_2(msg, stacklevel=2): + warnings.warn( + ( + "Deprecated (pylatexenc 2.0): {} " + "[see https://pylatexenc.readthedocs.io/en/latest/new-in-pylatexenc-2/]" + ).format(msg.strip()), + DeprecationWarning, + stacklevel=stacklevel + 1, + ) + + +# ------------------------------------------------------------------------------ + + +class LazyDict(MutableMapping): + r""" + A lazy dictionary that loads its data when it is first queried. + + This is used to store the legacy + :py:data:`pylatexenc.latexwalker.default_macro_dict` as well as + :py:data:`pylatexenc.latex2text.default_macro_dict` etc. Such that these + "dictionaries" are still exposed at the module-level, but the data is loaded + only if they are actually queried. + """ + + def __init__(self, generate_dict_fn): + self._full_dict = None + self._generate_dict_fn = generate_dict_fn + + def _ensure_instance(self): + if self._full_dict is not None: + return + self._full_dict = self._generate_dict_fn() + + def __getitem__(self, key): + self._ensure_instance() + return self._full_dict.__getitem__(key) + + def __setitem__(self, key, val): + self._ensure_instance() + return self._full_dict.__setitem__(key, val) + + def __delitem__(self, key): + self._ensure_instance() + return self._full_dict.__delitem__(key) + + def __iter__(self): + self._ensure_instance() + return iter(self._full_dict) + + def __len__(self): + self._ensure_instance() + return len(self._full_dict) + + def copy(self): + self._ensure_instance() + return self._full_dict.copy() + + def clear(self): + self._ensure_instance() + return self._full_dict.clear() + + +# ------------------------------------------------------------------------------ + + +class LineNumbersCalculator(object): + r""" + Utility to calculate line numbers. + """ + + def __init__(self, s): + super(LineNumbersCalculator, self).__init__() + + def find_all_new_lines(x): + # first line starts at the beginning of the string + yield 0 + k = 0 + while k < len(x): + k = x.find("\n", k) + if k == -1: + return + k += 1 + # s[k] is the character after the newline, i.e., the 0-th column + # of the new line + yield k + + self._pos_new_lines = list(find_all_new_lines(s)) + + def pos_to_lineno_colno(self, pos, as_dict=False): + r""" + Return the line and column number corresponding to the given `pos`. + + Return a tuple `(lineno, colno)` giving line number and column number. + Line numbers start at 1 and column number start at zero, i.e., the + beginning of the document (`pos=0`) has line and column number `(1,0)`. + If `as_dict=True`, then a dictionary with keys 'lineno', 'colno' is + returned instead of a tuple. + """ + + # find line number in list + + # line_no is the index of the last item in self._pos_new_lines that is <= pos. + line_no = bisect.bisect_right(self._pos_new_lines, pos) - 1 + assert line_no >= 0 and line_no < len(self._pos_new_lines) + + col_no = pos - self._pos_new_lines[line_no] + # 1+... so that line and column numbers start at 1 + if as_dict: + return {"lineno": 1 + line_no, "colno": col_no} + return (1 + line_no, col_no) diff --git a/good_ai/src/sus/external/pylatexenc/latex2text/__init__.py b/good_ai/src/sus/external/pylatexenc/latex2text/__init__.py new file mode 100644 index 0000000..d8ae865 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latex2text/__init__.py @@ -0,0 +1,1578 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +r""" +A simplistic, heuristic LaTeX code parser allowing to returns a text-only +approximation. Suitable, e.g. for indexing tex code in a database for full text +searching. + +The main class is :py:class:`LatexNodes2Text`. For a quick start, try:: + + from pylatexenc.latex2text import LatexNodes2Text + + latex = "... LaTeX code ..." + text = LatexNodes2Text().latex_to_text(latex) + +You may also use the command-line version of `latex2text`:: + + $ echo '\textit{italic} \`acc\^ented text' | latex2text + italic àccênted text + +""" + +from __future__ import print_function, unicode_literals # , absolute_import + +import inspect +import logging +import os +import re +import sys +import textwrap + + +def unicode(string): + return string + + +basestring = str +getfullargspec = inspect.getfullargspec + +from .. import _util, latexwalker, macrospec + +logger = logging.getLogger(__name__) + + +class MacroTextSpec(object): + """ + A specification of how to obtain a textual representation of a macro. + + .. py:attribute:: macroname + + The name of the macro (no backslash) + + .. py:attribute:: simplify_repl + + The replacement text of the macro invocation. This is either a string or + a callable: + + - If `simplify_repl` is a string, this string is used as the text + representation of this macro node. + + The string may contain a single '%s' replacement placeholder which + will be replaced by the concatenated textual representation of all + macro arguments. Alternatively, the string may contain '%()s' + (where `` is an integer) to refer to the n-th argument (starting + at '%(1)s'). You cannot mix the two %-formatting styles. + + - If `simplify_repl` is a callable, it should accept the corresponding + :py:class:`pylatexenc.latexwalker.LatexMacroNode` as an argument. + + The callable will be inspected to see what other arguments it + accepts. If it accepts an argument named `l2tobj`, the + :py:class:`LatexNodes2Text` instance is provided to that argument. + If it accepts an argument named `macroname`, the name of the macro is + provided to that argument. + + .. py:attribute:: discard + + If set to `True`, then the macro call is discarded, i.e., it is converted + to an empty string. + + + .. versionadded:: 2.0 + + The class :py:class:`MacroTextSpec` was introduced in `pylatexenc + 2.0` to succeed to the previously named `MacroDef` class. + """ + + def __init__(self, macroname, simplify_repl=None, discard=None): + super(MacroTextSpec, self).__init__() + self.macroname = macroname + self.discard = True if (discard is None) else discard + self.simplify_repl = simplify_repl + + +class EnvironmentTextSpec(object): + """ + A specification of how to obtain a textual representation of an environment. + + .. py:attribute:: environmentname + + The name of the environment + + .. py:attribute:: simplify_repl + + The replacement text of the environment. This is either a string or a + callable: + + - If `simplify_repl` is a string, this string is used as the text + representation of this environment node. + + The string may contain a single '%s' replacement placeholder, in + which the (processed) environment body will be substituted. + + Alternatively, the `simplify_repl` string may contain '%()s' + (where `` is an integer) to refer to the n-th argument after + ``\begin{environment}`` (starting at '%(1)s'). The body of the + environment has to be referred to with `%(body)s`. + + You cannot mix the two %-formatting styles. + + - If `simplify_repl` is a callable, it should accept the corresponding + :py:class:`pylatexenc.latexwalker.LatexEnvironmentNode` as an + argument. + + The callable will be inspected to see what other arguments it + accepts. If it accepts an argument named `l2tobj`, the + :py:class:`LatexNodes2Text` instance is provided to that argument. + If it accepts an argument named `environmentname`, the name of the + environment is provided to that argument. + + .. py:attribute:: discard + + If set to `True`, then the full environment is discarded, i.e., it is + converted to an empty string. + + + .. versionadded:: 2.0 + + The class :py:class:`EnvironmentTextSpec` was introduced in `pylatexenc + 2.0` to succeed to the previously named `EnvDef` class. + """ + + def __init__(self, environmentname, simplify_repl=None, discard=False): + super(EnvironmentTextSpec, self).__init__() + self.environmentname = environmentname + self.simplify_repl = simplify_repl + self.discard = discard + + +class SpecialsTextSpec(object): + """ + A specification of how to obtain a textual representation of latex specials. + + .. py:attribute:: specials_chars + + The sequence of special LaTeX characters + + .. py:attribute:: simplify_repl + + The replacement text for the given latex specials. This is either a + string or a callable: + + - If `simplify_repl` is a string, this string is used as the text + representation of this specials node. + + The string may contain a single '%s' replacement placeholder which + will be replaced by the concatenated textual representation of all + macro arguments. + + Alternatively, the string may contain '%()s' (where `` is an + integer) to refer to the n-th argument (starting at '%(1)s'). You + cannot mix the two %-formatting styles. + + - If `simplify_repl` is a callable, it should accept the corresponding + :py:class:`pylatexenc.latexwalker.LatexSpecialsNode` as an argument. + + The callable will be inspected to see what other arguments it + accepts. If it accepts an argument named `l2tobj`, the + :py:class:`LatexNodes2Text` instance is provided to that argument. + If it accepts an argument named `specials_chars`, the characters that + were parsed this "latex specials" node are provided to that argument. + + .. versionadded:: 2.0 + + Latex specials were introduced in `pylatexenc 2.0`. + """ + + def __init__(self, specials_chars, simplify_repl=None): + super(SpecialsTextSpec, self).__init__() + self.specials_chars = specials_chars + self.simplify_repl = simplify_repl + + +def EnvDef(envname, simplify_repl=None, discard=False): + r""" + .. deprecated:: 2.0 + + Instantiate a :py:class:`EnvironmentTextSpec` instead. + + Since `pylatexenc 2.0`, `EnvDef` is a function which returns a + :py:class:`~pylatexenc.macrospec.EnvironmentTextSpec` instance. In this + way the earlier idiom ``EnvDef(...)`` still works in `pylatexenc 2`. + """ + e = EnvironmentTextSpec( + environmentname=envname, simplify_repl=simplify_repl, discard=discard + ) + e.envname = e.environmentname + return e + + +def MacroDef(macname, simplify_repl=None, discard=None): + r""" + .. deprecated:: 2.0 + + Instantiate a :py:class:`MacroTextSpec` instead. + + Since `pylatexenc 2.0`, `MacroDef` is a function which returns a + :py:class:`~pylatexenc.macrospec.MacroTextSpec` instance. In this way + the earlier idiom ``MacroDef(...)`` still works in `pylatexenc 2`. + """ + m = MacroTextSpec(macroname=macname, simplify_repl=simplify_repl, discard=discard) + m.macname = m.macroname + return m + + +# +# NOTE: while internally documented (but not in the public docs), these fmt_*** +# functions should be considered internal API and should not be relied upon for +# the moment in production code. I intend to change some things in how common +# rendering procedures (for equations, for non-textual content that probably +# requires a placeholder, etc.) by some other means such as by extending the +# latex context database object directly. +# + + +def fmt_equation_environment(envnode, l2tobj): + r""" + Can be used as callback for display equation environments. + + .. versionadded:: 2.0 + + This function was introduced in `pylatexenc 2.0`. + """ + + return l2tobj.math_node_to_text(envnode) + + +def fmt_input_macro(macronode, l2tobj): + r""" + This function can be used as callback in :py:class:`MacroTextSpec` for + ``\input`` or ``\include`` macros. The `macronode` must be a macro node + with a single argument. If :py:meth:`set_tex_input_directory()` was called + with a nonempty input directory in the :py:class:`LatexNodes2Text` object, + then this method reads the contents of the file name in the macro argument + according to the provided settings. Otherwise, returns an empty string. + + .. versionadded:: 2.0 + + This function was introduced in `pylatexenc 2.0`. + """ + return l2tobj._input_node_simplify_repl(macronode) + + +def placeholder_node_formatter(placeholdertext, block=True): + r""" + This function returns a callable that can be used in + :py:class:`MacroTextSpec`, :py:class:`EnvironmentTextSpec`, or + :py:class:`SpecialsTextSpec` for latex nodes that do not have a good textual + representation, providing as text replacement the simple placeholder text + ``'< P L A C E H O L D E R T E X T >'``. + + If `block=True` (the default), the placeholder text is typeset in an + indented block on its own. Otherwise, it is typeset inline. + + .. versionadded:: 2.0 + + This function was introduced in `pylatexenc 2.0`. + """ + return lambda n, l2tobj, pht=placeholdertext: _do_fmt_placeholder_node( + pht, l2tobj, block=block + ) + + +def _do_fmt_placeholder_node(placeholdertext, l2tobj, block=True): + # spaces added so that database indexing doesn't index the word "array" or + # "pmatrix" + txt = "< " + " ".join(placeholdertext) + " >" + if block: + return l2tobj._fmt_indented_block(txt, indent=" ") + return " " + txt + " " + + +def fmt_placeholder_node(node, l2tobj): + r""" + This function can be used as callable in :py:class:`MacroTextSpec`, + :py:class:`EnvironmentTextSpec`, or :py:class:`SpecialsTextSpec` for latex + nodes that do not have a good textual representation. The text replacement + is the placeholder text + ``'< N A M E O F T H E M A C R O O R E N V I R O N M E N T >'``. + + .. versionadded:: 2.0 + + This function was introduced in `pylatexenc 2.0`. + """ + + for att in ("macroname", "environmentname", "specials_chars"): + if hasattr(node, att): + name = getattr(node, att) + break + else: + name = "" + + return _do_fmt_placeholder_node(name, l2tobj) + + +def fmt_matrix_environment_node(node, l2tobj): + r""" + This function can be used as a callable in :py:class:`EnvironmentTextSpec` + for matrix-like environments like ``\begin{bmatrix}...\end{bmatrix}``. + + The contents is parsed by separating columns with ``&``'s and rows with + ``\\``'s, and is rendered in the form ``[ a11 a12 ; a21 a22 ]``. + + .. versionadded:: 2.8 + + This function was introduced in `pylatexenc 2.8`. + """ + + class StateType: + def __init__(self): + self.matrix_rows = [] + self.buffer_this_column = [] + self.buffer_nodes = [] + + def add_content(self, node): + self.buffer_nodes.append(node) + + def new_column(self): + if self.buffer_nodes: + self.buffer_this_column.append( + l2tobj.nodelist_to_text(self.buffer_nodes).strip() + ) + self.buffer_nodes = [] + + def new_row(self): + self.new_column() + self.matrix_rows.append(self.buffer_this_column) + self.buffer_this_column = [] + + state = StateType() + + # iterate the nodelist and find column and row separators + for n in node.nodelist: + if n.isNodeType(latexwalker.LatexSpecialsNode) and n.specials_chars == "&": + # column separator + state.new_column() + continue + if n.isNodeType(latexwalker.LatexMacroNode) and n.macroname == "\\": + # row separator + state.new_row() + continue + state.add_content(n) + + state.new_row() # finish the last row + + # now format the contents as array -- + max_char_width = max((len(x) for row in state.matrix_rows for x in row)) + matrix_contents = "; ".join( + ( + " ".join((x.rjust(max_char_width, " ") for x in row)) + for row in state.matrix_rows + ) + ) + return "[ " + matrix_contents + " ]" + + +# +# see reference: https://unicode.org/charts/PDF/U1D400.pdf +# + +_fmt_math_style_offsets = { + "bold": (0x1D400, 0x1D41A), + "italic": (0x1D434, 0x1D44E), + "bold-italic": (0x1D468, 0x1D482), + "script": (0x1D49C, 0x1D4B6), + "bold-script": (0x1D4D0, 0x1D4EA), + "fraktur": (0x1D504, 0x1D51E), + "doublestruck": (0x1D538, 0x1D552), + "bold-fraktur": (0x1D56C, 0x1D586), + "sans": (0x1D5A0, 0x1D5BA), + "sans-bold": (0x1D5D4, 0x1D5EE), + "sans-italic": (0x1D608, 0x1D622), + "sans-bold-italic": (0x1D63C, 0x1D656), + "monospace": (0x1D670, 0x1D68A), +} +# account for "holes" in code point chart because some symbols have already +# been allocated earlier code points (see reference linked above) +_fmt_math_style_exceptions = { + "italic": { + ord("h"): chr(0x210E), # PLANK CONSTANT + }, + "script": { + ord("B"): chr(0x212C), + ord("E"): chr(0x2130), + ord("F"): chr(0x2131), + ord("H"): chr(0x210B), + ord("I"): chr(0x2110), + ord("L"): chr(0x2112), + ord("M"): chr(0x2133), + ord("R"): chr(0x211B), + ord("e"): chr(0x212F), + ord("g"): chr(0x210A), + ord("o"): chr(0x2134), + }, + "fraktur": { + ord("C"): chr(0x212D), + ord("H"): chr(0x210C), + ord("I"): chr(0x2111), + ord("R"): chr(0x211C), + ord("Z"): chr(0x2128), + }, + "doublestruck": { + ord("C"): chr(0x2102), + ord("H"): chr(0x210D), + ord("N"): chr(0x2115), + ord("P"): chr(0x2119), + ord("Q"): chr(0x211A), + ord("R"): chr(0x211D), + ord("Z"): chr(0x2124), + }, +} + +_oA, _oZ, _oa, _oz = ord("A"), ord("Z"), ord("a"), ord("z") + + +def _fmt_math_style_char(c, style): + oc = ord(c) + z = _fmt_math_style_exceptions.get(style, {}).get(oc, None) + if z is not None: + return z + + offset_up, offset_lo = _fmt_math_style_offsets.get( + style, + ( + _oA, + _oa, + ), + ) + + if oc >= _oA and oc <= _oZ: + return chr(offset_up + oc - _oA) + if oc >= _oa and oc <= _oz: + return chr(offset_lo + oc - _oa) + + # don't know how to handle this char + return c + + +if sys.maxunicode < 0x10FFFF: + # narrow python build, disable math alphabets. + _fmt_math_style_char = lambda c, style: c + + +def fmt_math_text_style(text, style): + r""" + Return the text with letters replaced by unicode characters so that the + style `style` is applied. (We use the unicode math alphanumeric symbols, + see `https://unicode.org/charts/PDF/U1D400.pdf`_.) + + The `style` must be one of 'bold', 'italic', 'bold-italic', 'script', + 'bold-script', 'fraktur', 'doublestruck', 'bold-fraktur', 'sans', + 'sans-bold', 'sans-italic', 'sans-bold-italic', or 'monospace'. + + The character `c` is essentially expected to be an ascii letter, and any + other character will be returned unchanged. (Possible exceptions might be + implemented in the future, for instance to implement the double-struck + one/identity operator ``\mathbbm{1}``.) + """ + return "".join((_fmt_math_style_char(c, style=style) for c in text)) + + +def get_default_latex_context_db(): + r""" + Return a :py:class:`pylatexenc.macrospec.LatexContextDb` instance + initialized with a collection of text replacements for known macros and + environments. + + TODO: clean up and document categories. + + If you want to add your own definitions, you should use the + :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()` + method. If you would like to override some definitions, use that method + with the argument `prepend=True`. See docs for + :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()`. + + If there are too many macro/environment definitions, or if there are some + irrelevant ones, you can always filter the returned database using + :py:meth:`pylatexenc.macrospec.LatexContextDb.filter_context()`. + + .. versionadded:: 2.0 + + The :py:class:`pylatexenc.macrospec.LatexContextDb` class as well as this + method, were all introduced in `pylatexenc 2.0`. + """ + db = macrospec.LatexContextDb() + + from ._defaultspecs import specs + + for cat, catspecs in specs: + db.add_context_category( + cat, + macros=catspecs["macros"], + environments=catspecs["environments"], + specials=catspecs["specials"], + ) + + return db + + +default_macro_dict = _util.LazyDict( + generate_dict_fn=lambda: dict( + [(m.macroname, m) for m in get_default_latex_context_db().iter_macro_specs()] + ) +) +r""" +.. deprecated:: 2.0 + + Use :py:func:`get_default_latex_context_db()` instead, or create your own + :py:class:`pylatexenc.macrospec.LatexContextDb` object. + + +Provide an access to the default macro text replacement specs for `latex2text` +in a form that is compatible with `pylatexenc 1.x`\ 's `default_macro_dict` +module-level dictionary. + +This is implemented using a custom lazy mutable mapping, which behaves just like +a regular dictionary but that loads the data only once the dictionary is +accessed. In this way the default latex specs into a python dictionary unless +they are actually queried or modified, and thus users of `pylatexenc 2.0` that +don't rely on the default macro/environment definitions shouldn't notice any +decrease in performance. +""" + +default_env_dict = _util.LazyDict( + generate_dict_fn=lambda: dict( + [ + (m.environmentname, m) + for m in get_default_latex_context_db().iter_environment_specs() + ] + ) +) +r""" +.. deprecated:: 2.0 + + Use :py:func:`get_default_latex_context_db()` instead, or create your own + :py:class:`pylatexenc.macrospec.LatexContextDb` object. + + +Provide an access to the default environment text replacement specs for +`latex2text` in a form that is compatible with `pylatexenc 1.x`\ 's +`default_macro_dict` module-level dictionary. + +This is implemented using a custom lazy mutable mapping, which behaves just like +a regular dictionary but that loads the data only once the dictionary is +accessed. In this way the default latex specs into a python dictionary unless +they are actually queried or modified, and thus users of `pylatexenc 2.0` that +don't rely on the default macro/environment definitions shouldn't notice any +decrease in performance. +""" + + +default_text_replacements = () +r""" +.. deprecated:: 2.0 + + Text replacements are deprecated since `pylatexenc 2.0` with the advent of + "latex specials". See :py:meth:`LatexNodes2Text.apply_text_replacements()` + for a quick solution to keep existing code working if it uses custom text + replacements. +""" + + +# ------------------------------------------------------------------------------ + +_strict_latex_spaces_predef = { + "based-on-source": { + "between-macro-and-chars": False, + "between-latex-constructs": False, + "after-comment": False, + "in-equations": None, + }, + "macros": { + "between-macro-and-chars": True, + "between-latex-constructs": True, + "after-comment": False, + "in-equations": "based-on-source", + }, + "except-in-equations": { + "between-macro-and-chars": True, + "between-latex-constructs": True, + "after-comment": True, + "in-equations": "based-on-source", + }, +} + + +def _parse_strict_latex_spaces_dict(strict_latex_spaces): + d = { + "between-macro-and-chars": False, + "between-latex-constructs": False, + "after-comment": False, + "in-equations": None, + } + if strict_latex_spaces is None: + return d + elif strict_latex_spaces is False: + # "False" == the actual default for non-strict latex spaces == "macros" + return _strict_latex_spaces_predef["macros"] + elif strict_latex_spaces is True: + return dict([(k, True) for k in d.keys()]) + elif isinstance(strict_latex_spaces, dict): + d.update(strict_latex_spaces) + return d + elif isinstance(strict_latex_spaces, basestring): + if strict_latex_spaces == "on": + return _parse_strict_latex_spaces_dict(True) + if strict_latex_spaces == "off": + return _parse_strict_latex_spaces_dict(False) + if strict_latex_spaces not in _strict_latex_spaces_predef: + raise ValueError( + "invalid value for strict_latex_spaces preset: {}".format( + strict_latex_spaces + ) + ) + + if strict_latex_spaces == "default": # deprecated -- report this + # compatibility with pylatexenc 1.x, but it is no longer the default!! + _util.pylatexenc_deprecated_2( + "The value 'default' for `strict_latex_spaces=` in LatexNodes2Text() " + "is deprecated. The actual default changed to 'macros', and for " + "backwards compatibility the obsolete value 'default' still refers to " + "the earlier default which is now called 'based-on-source'.", + stacklevel=4, + ) + strict_latex_spaces = "based-on-source" + + return _strict_latex_spaces_predef[strict_latex_spaces] + else: + raise ValueError( + "Invalid value for strict_latex_spaces: {!r}".format(strict_latex_spaces) + ) + + +class LatexNodes2Text(object): + r""" + Simplistic Latex-To-Text Converter. + + This class parses a nodes structure generated by the :py:mod:`latexwalker` module, + and creates a text representation of the structure. + + It is capable of parsing ``\input`` directives safely, see + :py:meth:`set_tex_input_directory()` and :py:meth:`read_input_file()`. By default, + ``\input`` and ``\include`` directives are ignored. + + Arguments to the constructor: + + - `latex_context_db` is a :py:class:`pylatexenc.macrospec.LatexContextDb` + class storing a collection of rules for converting macros, environments, + and other latex specials to text. The `LatexContextDb` should contain + specifications via :py:class:`MacroTextSpec`, + :py:class:`EnvironmentTextSpec`, and :py:class:`SpecialsTextSpec` objects. + + The default latex context database can be obtained using + :py:func:`get_default_latex_context_db()`. + + Additional keyword arguments are flags which may influence the behavior: + + - `math_mode='text'|'with-delimiters'|'verbatim'|'remove'`: Specify how to + treat chunks of LaTeX code that correspond to math modes. If 'text' (the + default), then the math mode contents is incorporated as normal text. If + 'with-delimiters', the content is incorporated as normal text but it is + still included in the original math-mode delimiters, such as '$...$'. If + 'verbatim', then the math mode chunk is kept verbatim, including the + delimiters. The value 'remove' means to remove the math mode sections + entirely and not to produce any replacement text. + + - `keep_comments=True|False`: If set to `True`, then LaTeX comments are kept + (including the percent-sign); otherwise they are discarded. (By default + this is `False`) + + - `fill_text`: If set to `True` or to a positive integer, then the + whitespace of LaTeX char blocks is re-layed out to fill at the given + number of characters or 80 by default. The fill is by far not perfect, + but the resulting text might be slightly more readable. + + - `strict_latex_spaces=True|False`: If set to `True`, then we follow closely + LaTeX's handling of whitespace. For instance, whitespace following a bare + macro (i.e. without any delimiting characters like '{') is + consumed/removed. If set to `False` (the default), then some liberties + are taken with respect to whitespace [hopefully making the result slightly + more aesthetic, but this behavior is mostly there for historical reasons]. + + You may also use one of the presets + `strict_latex_spaces='based-on-source'|'macros'|'except-in-equations'`, + which allow for finer control of how whitespace is handled: + + - The value 'based-on-source' is the option that is furthest from + latex's behavior with spaces, and takes liberties in incuding spaces + that are present in the source file in several situations where LaTeX + would remove them, including after macros. This is meant to be + hopefully slightly more aesthetic. However, this option might + inadvertently break up words: For instance:: + + Sk\l odowska + + would be replaced by:: + + Skł odowska + + - The value 'macros' is the same as specifying + `strict_latex_spaces=False`, and it is the default. It will make + macros and other sequences of LaTeX constructions obey LaTeX space + rules, but will keep indentations after comments and keep more liberal + whitespace rules in equations for a hopefully more aesthetic result. + + - The 'except-in-equations' preset goes as you would expect, setting + strict latex spacing only outside of equation contexts. + + Finally, the argument `strict_latex_spaces` may also be set to a + dictionary with keys 'between-macro-and-chars', 'after-comment', + 'between-latex-constructs', and 'in-equations', with individual values + either `True` or `False`, dictating whitespace behavior in specific cases + (`True` indicates strict latex behavior). The value for 'in-equations' + may even be another dictionary with the same keys to override values in + equations. A value of `False` for 'in-equation' has the same meaning as + 'macros'. + + .. versionchanged:: 2.0 + + Since `pylatexenc 2.0`, the default value of `strict_latex_spaces` is + 'macros', and no longer 'based-on-source'. + + .. deprecated:: 2.0 + + The value 'default' is also accepted, but it is no longer the default! + It is an alias for 'based-on-source' + + .. versionchanged:: 2.6 + + In `pylatexenc` versions 2.0–2.5, contrary to the documentation, the + default value of `strict_latex_spaces` was actually still + 'based-on-source'. This bug was fixed in version 2.6, so that now, the + default setting is actually 'macros'. + + - `keep_braced_groups=True|False`: If set to `True`, then braces delimiting + a TeX group ``{Like this}`` will be kept in the output, with the contents + of the group converted to text as usual. (By default this is `False`) + + - `keep_braced_groups_minlen=`: If `keep_braced_groups` is set to + `True`, then we keep braced groups only if their contents length (after + conversion to text) is longer than the given value. E.g., if + `keep_braced_groups_minlen=2`, then ``{\'e}tonnant`` still goes to + ``étonnant`` but ``{\'etonnant}`` + becomes ``{étonnant}``. + + .. versionadded: 1.4 + + Added the `strict_latex_spaces`, `keep_braced_groups`, and + `keep_braced_groups_minlen` flags + + .. versionadded: 2.0 + + Added the `math_mode=` flag to replace the poorly designed + `keep_inline_math=` flag; + + Added the `fill_text=` flag. + + Additionally, the following arguments are accepted for backwards compatibility: + + - `keep_inline_math=True|False`: Obsolete since `pylatexenc 2`. If set to + `True`, then this is the same as `math_mode='verbatim'`, and if set to + `False`, this is the same as `math_mode='text'`. + + .. deprecated:: 2.0 + + The `keep_inline_math=` option is deprecated because it had a weird + behavior and was poorly implemented, especially given that a similarly + named option in :py:class:`LatexWalker` had a different effect. See + issue :issue:`14`. + + - `text_replacements` this argument is ignored starting from `pylatexenc 2`. + + .. deprecated:: 2.0 + + Text replacements are no longer made at the end of the text conversion. + This feature is replaced by the concept of LaTeX specials---see, e.g., + :py:class:`pylatexenc.latexwalker.LatexSpecialsNode`. + + To keep existing code working, add a call to + :py:meth:`apply_text_replacements()` immediately after + :py:meth:`nodelist_to_text()` to achieve the same effect as in + `pylatexenc 1.x`. See :py:meth:`apply_text_replacements()`. + + - `env_dict`, `macro_dict`: Obsolete since `pylatexenc 2`. If set, they are + dictionaries of known environment and macro definitions. They default to + :py:data:`default_env_dict` and :py:data:`default_macro_dict`, + respectively. + + .. deprecated:: 2.0 + + You should now use the more powerful option `latex_context_db=`. You + cannot specify both `macro_list` (or `env_list`) and + `latex_context_db`. + """ + + def __init__(self, latex_context=None, **flags): + super(LatexNodes2Text, self).__init__() + + if latex_context is None: + if "macro_dict" in flags or "env_dict" in flags: + # LEGACY -- build a latex context using the given macro_dict + _util.pylatexenc_deprecated_2( + "The `macro_dict=...` and `env_dict=...` options in LatexNodes2Text() are " + "obsolete since pylatexenc 2. They will still work, but please consider " + "using instead the more versatile option `latex_context=...`." + ) + + macro_dict = flags.pop("macro_dict", []) + env_dict = flags.pop("env_dict", []) + + latex_context = macrospec.LatexContextDb() + latex_context.add_context_category( + "custom", + macros=macro_dict.values(), + environments=env_dict.values(), + specials=[], + ) + + else: + # default -- use default + latex_context = get_default_latex_context_db() + + self.latex_context = latex_context + + self.tex_input_directory = None + self.strict_input = True + + if "keep_inline_math" in flags: + if "math_mode" in flags: + raise TypeError( + "Cannot specify both math_mode= and keep_inline_math= " + "for LatexNodes2Text()" + ) + _util.pylatexenc_deprecated_2( + "The keep_inline_math=... option in LatexNodes2Text() has been replaced by " + "the math_mode=... option." + ) + self.math_mode = "verbatim" if flags.pop("keep_inline_math") else "text" + else: + self.math_mode = flags.pop("math_mode", "text") + + if self.math_mode not in ("text", "with-delimiters", "verbatim", "remove"): + raise ValueError( + "math_mode= option must be one of 'text', 'with-delimiters', " + "'verbatim', 'remove'" + ) + + self.keep_comments = flags.pop("keep_comments", False) + + strict_latex_spaces = flags.pop("strict_latex_spaces", False) + self.strict_latex_spaces = _parse_strict_latex_spaces_dict(strict_latex_spaces) + + self.keep_braced_groups = flags.pop("keep_braced_groups", False) + self.keep_braced_groups_minlen = flags.pop("keep_braced_groups_minlen", 2) + + self.fill_text = flags.pop("fill_text", None) + if not self.fill_text: # None, 0, False, or false-ish + self.fill_text = None + if self.fill_text is True: # exactly boolean true, not an int + self.fill_text = 80 + + if "text_replacements" in flags: + del flags["text_replacements"] + _util.pylatexenc_deprecated_2( + "The text_replacements= argument is ignored since pylatexenc 2. " + "To keep existing code working, add a call to " + "`LatexNodes2Text.apply_text_replacements()`. " + 'New code should use "latex specials" instead.' + ) + + if flags: + # any flags left which we haven't recognized + logger.warning( + "LatexNodes2Text(): Unknown flag(s) encountered: %r", list(flags.keys()) + ) + + def set_tex_input_directory( + self, tex_input_directory, latex_walker_init_args=None, strict_input=True + ): + """ + Set where to look for input files when encountering the ``\\input`` or + ``\\include`` macro. + + Alternatively, you may also override :py:meth:`read_input_file()` to + implement a custom file lookup mechanism. + + The argument `tex_input_directory` is the directory relative to which to + search for input files. + + If `strict_input` is set to `True`, then we always check that the + referenced file lies within the subtree of `tex_input_directory`, + prohibiting for instance hacks with '..' in filenames or using symbolic + links to refer to files out of the directory tree. + + The argument `latex_walker_init_args` allows you to specify the parse + flags passed to the constructor of + :py:class:`pylatexenc.latexwalker.LatexWalker` when parsing the input + file. + """ + self.tex_input_directory = tex_input_directory + self.latex_walker_init_args = ( + latex_walker_init_args if latex_walker_init_args else {} + ) + self.strict_input = strict_input + + def read_input_file(self, fn): + """ + This method may be overridden to implement a custom lookup mechanism when + encountering ``\\input`` or ``\\include`` directives. + + The default implementation looks for a file of the given name relative + to the directory set by :py:meth:`set_tex_input_directory()`. If + `strict_input=True` was set, we ensure strictly that the file resides in + a subtree of the reference input directory (after canonicalizing the + paths and resolving all symlinks). + + If `set_tex_input_directory()` was not called, or if it was called with + a value of `None`, then no file system access is attempted an an empty + string is returned. + + You may override this method to obtain the input data in however way you + see fit. In that case, a call to `set_tex_input_directory()` may not be + needed as that function simply sets properties which are used by the + default implementation of `read_input_file()`. + + This function accepts the referred filename as argument (the argument to + the ``\\input`` macro), and should return a string with the file + contents (or generate a warning or raise an error). + """ + + if self.tex_input_directory is None: + return "" + + fnfull = os.path.realpath(os.path.join(self.tex_input_directory, fn)) + if self.strict_input: + # make sure that the input file is strictly within dirfull, and + # didn't escape with '../..' tricks or via symlinks. + dirfull = os.path.realpath(self.tex_input_directory) + if not fnfull.startswith(dirfull): + logger.warning( + "Can't access path '%s' leading outside of mandated directory " + "[strict input mode]", + fn, + ) + return "" + + if not os.path.exists(fnfull) and os.path.exists(fnfull + ".tex"): + fnfull = fnfull + ".tex" + if not os.path.exists(fnfull) and os.path.exists(fnfull + ".latex"): + fnfull = fnfull + ".latex" + if not os.path.isfile(fnfull): + logger.warning("Error, file doesn't exist: '%s'", fn) + return "" + + logger.debug("Reading input file %r", fnfull) + + try: + with open(fnfull) as f: + return f.read() + except IOError as e: + logger.warning("Error, can't access '%s': %s", fn, e) + return "" + + def _input_node_simplify_repl(self, n): + # + # recurse into files upon '\input{}' + # + + if len(n.nodeargs) != 1: + logger.warning( + "Expected exactly one argument for '\\input' ! Got = %r", n.nodeargs + ) + + inputtex = self.read_input_file(self.nodelist_to_text([n.nodeargs[0]]).strip()) + + if not inputtex: + return "" + + return self.nodelist_to_text( + latexwalker.LatexWalker( + inputtex, **self.latex_walker_init_args + ).get_latex_nodes()[0] + ) + + def latex_to_text(self, latex, **parse_flags): + """ + Parses the given `latex` code and returns its textual representation. + + This is equivalent to constructing a + :py:class:`pylatexenc.latexwalker.LatexWalker` with the given `latex` + string, calling its method + :py:meth:`~pylatexenc.latexwalker.LatexWalker.get_latex_nodes()`, and + providing the outcome to :py:meth:`nodelist_to_text()`. + + The `parse_flags` are keyword arguments to provide to the + :py:class:`pylatexenc.latexwalker.LatexWalker` constructor. + """ + return self.nodelist_to_text( + latexwalker.LatexWalker(latex, **parse_flags).get_latex_nodes()[0] + ) + + def nodelist_to_text(self, nodelist): + """ + Extracts text from a node list. `nodelist` is a list of `latexwalker` nodes, + typically returned by + :py:meth:`pylatexenc.latexwalker.LatexWalker.get_latex_nodes()`. + + This function basically applies `node_to_text()` to each node and + concatenates the results into one string. (This is not quite actually + the case, since we take some care as to where we add whitespace + according to the class options.) + """ + + s = "" + prev_node = None + for node in nodelist: + if self._is_bare_macro_node(prev_node) and node.isNodeType( + latexwalker.LatexCharsNode + ): + + if not self.strict_latex_spaces["between-macro-and-chars"]: + # after a macro with absolutely no arguments, include + # post_space in output by default if there are other chars + # that follow. This is for more breathing space (especially + # in equations(?)), and for compatibility with earlier + # versions of pylatexenc (<= 1.3). This is NOT LaTeX' + # default behavior (see issue #11), so only do this if the + # corresponding `strict_latex_spaces=` flag is set. + s += prev_node.macro_post_space + + last_nl_pos = s.rfind("\n") + if last_nl_pos != -1: + textcol = len(s) - last_nl_pos - 1 + else: + textcol = len(s) + + s += self.node_to_text(node, textcol=textcol) + + prev_node = node + + return s + + def node_to_text(self, node, prev_node_hint=None, textcol=0): + """ + Return the textual representation of the given `node`. + + If `prev_node_hint` is specified, then the current node is formatted + suitably as following the node given in `prev_node_hint`. This might + affect how much space we keep/discard, etc. + """ + if node is None: + return "" + + # ### It doesn't look like we use prev_node_hint at all. Eliminate at + # ### some point? + + if node.isNodeType(latexwalker.LatexCharsNode): + return self.chars_node_to_text(node, textcol=textcol) + + if node.isNodeType(latexwalker.LatexCommentNode): + return self.comment_node_to_text(node) + + if node.isNodeType(latexwalker.LatexGroupNode): + return self.group_node_to_text(node) + + if node.isNodeType(latexwalker.LatexMacroNode): + return self.macro_node_to_text(node) + + if node.isNodeType(latexwalker.LatexEnvironmentNode): + return self.environment_node_to_text(node) + + if node.isNodeType(latexwalker.LatexSpecialsNode): + return self.specials_node_to_text(node) + + if node.isNodeType(latexwalker.LatexMathNode): + return self.math_node_to_text(node) + + logger.warning("LatexNodes2Text.node_to_text(): Unknown node: %r", node) + + # discard anything else. + return "" + + def chars_node_to_text(self, node, textcol=0): + r""" + Return the textual representation of the given `node` representing a block + of simple latex text with no special characters or macros. The `node` + is :py:class:`~pylatexenc.latexwalker.LatexCharsNode`. + """ + # Unless in strict latex spaces mode, ignore nodes consisting only + # of empty chars, as this tends to produce too much space... These + # have been inserted by LatexWalker() in some occasions to keep + # track of all relevant pre_space of tokens, such as between two + # braced groups ("{one} {two}") or other such situations. + content = node.chars + if self.fill_text: # None or column width + content = self.do_fill_text(content, textcol=textcol) + if ( + not self.strict_latex_spaces["between-latex-constructs"] + and len(content.strip()) == 0 + ): + return "" + return content + + def comment_node_to_text(self, node): + r""" + Return the textual representation of the given `node` representing a latex + comment. The `node` is + :py:class:`~pylatexenc.latexwalker.LatexCommentNode`. + """ + if self.keep_comments: + if self.strict_latex_spaces["after-comment"]: + nl = "\n" + if node.comment_post_space == "": + # this happens if two newlines follow a comment---the + # comment_post_space is empty, and the \n\n is reported as a + # char node to notify that there is a new paragraph. + nl = "" + return "%" + node.comment + nl + else: + # default spaces, i.e., keep what spaces were already there + # after the comment + return "%" + node.comment + node.comment_post_space + else: + if self.strict_latex_spaces["after-comment"]: + return "" + else: + # default spaces, i.e., keep what spaces were already there + # after the comment. This can be useful to preserve + # e.g. indentation of the next line + return node.comment_post_space + + def group_node_to_text(self, node): + r""" + Return the textual representation of the given `node` representing a latex + group. The `node` is + :py:class:`~pylatexenc.latexwalker.LatexGroupNode`. + """ + contents = self._groupnodecontents_to_text(node) + if self.keep_braced_groups and len(contents) >= self.keep_braced_groups_minlen: + return node.delimiters[0] + contents + node.delimiters[1] + return contents + + def macro_node_to_text(self, node): + r""" + Return the textual representation of the given `node` representing a latex + macro invocation. The `node` is + :py:class:`~pylatexenc.latexwalker.LatexMacroNode`. + """ + # get macro behavior definition. + macroname = node.macroname + mac = self.latex_context.get_macro_spec(macroname) + if mac is None: + # default for unknown macros + mac = MacroTextSpec("", discard=True) + + def get_macro_str_repl(node, macroname, mac): + if mac.simplify_repl: + return self.apply_simplify_repl( + node, mac.simplify_repl, what=r"macro '\%s'" % (macroname) + ) + if mac.discard: + return "" + a = [] + if node.nodeargd and node.nodeargd.argnlist: + a = node.nodeargd.argnlist + return "".join([self._groupnodecontents_to_text(n) for n in a]) + + macrostr = get_macro_str_repl(node, macroname, mac) + return macrostr + + def environment_node_to_text(self, node): + r""" + Return the textual representation of the given `node` representing a full + latex environment. The `node` is + :py:class:`~pylatexenc.latexwalker.LatexEnvironmentNode`. + """ + # get environment behavior definition. + environmentname = node.environmentname + envdef = self.latex_context.get_environment_spec(environmentname) + if envdef is None: + # default for unknown environments + envdef = EnvironmentTextSpec("", discard=False) + + if envdef.simplify_repl: + return self.apply_simplify_repl( + node, envdef.simplify_repl, what="environment '%s'" % (environmentname) + ) + if envdef.discard: + return "" + + return self.nodelist_to_text(node.nodelist) + + def specials_node_to_text(self, node): + r""" + Return the textual representation of the given `node` representing special a + latex character (or characters). The `node` is + :py:class:`~pylatexenc.latexwalker.LatexSpecialsNode`. + """ + # get the specials text spec + specials_chars = node.specials_chars + sspec = self.latex_context.get_specials_spec(specials_chars) + if sspec is None: + # no corresponding spec, leave the special chars unchanged: + return specials_chars + + def get_specials_str_repl(node, specials_chars, spec): + if spec.simplify_repl: + return self.apply_simplify_repl( + node, spec.simplify_repl, what="specials '%s'" % (specials_chars) + ) + if spec.discard: + return "" + if node.nodeargd and node.nodeargd.argnlist: + a = node.nodeargd.argnlist + return "".join([self._groupnodecontents_to_text(n) for n in a]) + + s = get_specials_str_repl(node, specials_chars, sspec) + return s + + def math_node_to_text(self, node): + r""" + Return the textual representation of the given `node` representing a block + of math mode latex. The `node` is either a + :py:class:`~pylatexenc.latexwalker.LatexMathNode` or a + :py:class:`~pylatexenc.latexwalker.LatexEnvironmentNode`. + + This method is responsible for honoring the `math_mode=...` option + provided to the constructor. + """ + + if self.math_mode == "verbatim": + if ( + node.isNodeType(latexwalker.LatexEnvironmentNode) + or node.displaytype == "display" + ): + return self._fmt_indented_block(node.latex_verbatim(), indent="") + else: + return node.latex_verbatim() + + elif self.math_mode == "remove": + return "" + + elif self.math_mode == "with-delimiters": + with _PushEquationContext(self): + content = self.nodelist_to_text(node.nodelist).strip() + if node.isNodeType(latexwalker.LatexMathNode): + delims = node.delimiters + else: # environment node + delims = ( + r"\begin{%s}" % (node.environmentname), + r"\end{%s}" % (node.environmentname), + ) + if ( + node.isNodeType(latexwalker.LatexEnvironmentNode) + or node.displaytype == "display" + ): + return ( + delims[0] + self._fmt_indented_block(content, indent="") + delims[1] + ) + else: + return delims[0] + content + delims[1] + + elif self.math_mode == "text": + with _PushEquationContext(self): + content = self.nodelist_to_text(node.nodelist).strip() + if ( + node.isNodeType(latexwalker.LatexEnvironmentNode) + or node.displaytype == "display" + ): + return self._fmt_indented_block(content) + else: + return content + + else: + raise RuntimeError("unknown math_mode={} !".format(self.math_mode)) + + def do_fill_text(self, text, textcol=0): + # keep trailing whitespace to have whitespace between macros in text as + # in "see \ref{...} and blah blah" + head_ws = re.search(r"^\s*", text).group() + head_par = "\n\n" if ("\n\n" in head_ws) else "" + # head_nl = '\n' if (not head_par and '\n' in head_ws) else '' + trail_ws = re.search(r"\s*$", text).group() + trail_par = "\n\n" if ("\n\n" in trail_ws) else "" + # trail_nl = '\n' if (not trail_par and '\n' in trail_ws) else '' + text = text.strip() + + def fill_chunk(x, textcol): + # head_ws = ' ' if textcol>0 and x[0:1].isspace() else '' + # trail_ws = ' ' if x[-1:].isspace() else '' + head_ws, trail_ws = "", "" + x = x.strip() + if textcol >= self.fill_text - 4: + return "\n" + textwrap.fill(x, self.fill_text) + trail_ws + else: + return ( + head_ws + + textwrap.fill(x, self.fill_text, initial_indent="X" * textcol)[ + textcol: + ] + + trail_ws + ) + + rawchunks = re.compile(r"\n{2,}").split(text) + + chunks = [ + thechunk + for (j, thechunk) in ( + (j, fill_chunk(x, textcol if j == 0 else 0)) + for j, x in enumerate(rawchunks) + ) + if thechunk.strip() + ] + + return ( + head_par + + (" " if textcol > 0 and head_ws and not head_par else "") + + "\n\n".join(chunks) + + (" " if trail_ws and not trail_par else "") + + trail_par + ) + + def apply_simplify_repl(self, node, simplify_repl, what): + r""" + Utility to get the replacement text associated with a `node` for which we + have a `simplify_repl` object (given by e.g. a MacroTextSpec or + similar). + + The argument `what` is used in error messages. + """ + if callable(simplify_repl): + kwargs = {} + fn_args = getfullargspec(simplify_repl)[0] + if "l2tobj" in fn_args: + # callable accepts an argument named 'l2tobj', provide pointer to self + kwargs["l2tobj"] = self + if ( + node.isNodeType(latexwalker.LatexEnvironmentNode) + and "environmentname" in fn_args + ): + kwargs["environmentname"] = node.environmentname + if node.isNodeType(latexwalker.LatexMacroNode) and "macroname" in fn_args: + kwargs["macroname"] = node.macroname + if ( + node.isNodeType(latexwalker.LatexSpecialsNode) + and "specials_chars" in fn_args + ): + kwargs["specials_chars"] = node.specials_chars + + r = simplify_repl(node, **kwargs) + if r: + return r + return "" # don't return None + + if "%" in simplify_repl and len(simplify_repl) != 1: + # if simplify_repl contains a '%' sign then we will look for %-based + # formatting placeholder(s), except if simplify_repl is the string + # '%' itself (checked above with "len(simplify_repl)!=1") in which + # case it is a literal replacement percent symbol. + + nodeargs = [] + if node.nodeargd and node.nodeargd.argnlist: + nodeargs = node.nodeargd.argnlist + + has_percent_s = re.search("(^|[^%])(%%)*%s", simplify_repl) + + if node.isNodeType(latexwalker.LatexEnvironmentNode): + if has_percent_s: + x = (self.nodelist_to_text(node.nodelist),) + else: + x = dict( + (str(1 + j), val) + for j, val in enumerate( + self._groupnodecontents_to_text(nn) for nn in nodeargs + ) + ) + x.update(body=self.nodelist_to_text(node.nodelist)) + elif has_percent_s: + x = tuple([self._groupnodecontents_to_text(nn) for nn in nodeargs]) + else: + x = dict( + (str(1 + j), val) + for j, val in enumerate( + self._groupnodecontents_to_text(nn) for nn in nodeargs + ) + ) + + try: + return simplify_repl % x + except (TypeError, ValueError, KeyError): + logger.warning( + "WARNING: Error in configuration: {} failed its substitution!".format( + what + ) + ) + return simplify_repl # too bad, keep the percent signs as they are... + return simplify_repl + + def _fmt_indented_block(self, contents, indent=" " * 4): + block = "\n" + indent + contents.replace("\n", "\n" + indent) + "\n" + if self.fill_text: + # additional newlines because neighboring text gets trimmed + block = "\n" + block + "\n" + return block + + def _is_bare_macro_node(self, node): + return ( + node is not None + and node.isNodeType(latexwalker.LatexMacroNode) + and node.nodeoptarg is None + and len(node.nodeargs) == 0 + ) + + def _groupnodecontents_to_text(self, groupnode): + if groupnode is None: + return "" + if not groupnode.isNodeType(latexwalker.LatexGroupNode): + return self.node_to_text(groupnode) + return self.nodelist_to_text(groupnode.nodelist) + + def node_arg_to_text(self, node, k): + r""" + Return the textual representation of the `k`\ -th argument of the given + `node`. This might be useful for substitution lambdas in macro and + environment specs. + """ + if node.nodeargd and node.nodeargd.argnlist: + return self._groupnodecontents_to_text(node.nodeargd.argnlist[k]) + return "" + + def apply_text_replacements(self, s, text_replacements): + r""" + Convenience function for code that used `text_replacements=` in `pylatexenc + 1.x`. + + If you used custom `text_replacements=` in `pylatexenc 1.x` then you + will have to change:: + + # pylatexenc 1.x with text_replacements + text_replacements = ... + l2t = LatexNodes2Text(..., text_replacements=text_replacements) + text = l2t.nodelist_to_text(...) + + to:: + + # pylatexenc 2 text_replacements compatibility code + text_replacements = ... + l2t = LatexNodes2Text(...) + temp = l2t.nodelist_to_text(...) + text = l2t.apply_text_replacements(temp, text_replacements) + + as a quick fix. It is recommended however to treat text replacements + instead as "latex specials". (Otherwise the brutal text replacements + might act on text generated from macros and environments and give + unwanted results.) See :py:class:`pylatexenc.macrospec.SpecialsSpec` + and :py:class:`SpecialsTextSpec`. + + .. deprecated:: 2.0 + + The `apply_text_replacements()` method was introduced in `pylatexenc + 2.0` as a deprecated method. You can use it as a quick fix to make + existing code run as it did in `pylatexenc 1.x`. Its use is however + not recommended for new code. You should use "latex specials" + instead for characters that have special LaTeX meaning. + """ + + # perform suitable replacements + for pattern, replacement in text_replacements: + if hasattr(pattern, "sub"): + s = pattern.sub(replacement, s) + else: + s = s.replace(pattern, replacement) + + return s + + +class _PushEquationContext(latexwalker._PushPropOverride): + def __init__(self, l2t): + + new_strict_latex_spaces = None + if l2t.strict_latex_spaces["in-equations"] is not None: + new_strict_latex_spaces = _parse_strict_latex_spaces_dict( + l2t.strict_latex_spaces["in-equations"] + ) + + super(_PushEquationContext, self).__init__( + l2t, "strict_latex_spaces", new_strict_latex_spaces + ) + + +# ------------------------------------------------------------------------------ + + +def latex2text( + content, tolerant_parsing=False, keep_inline_math=False, keep_comments=False +): + """ + Heuristic conversion of LaTeX content `content` to unicode text. + + .. deprecated:: 1.0 + Please use :py:class:`LatexNodes2Text` instead. + """ + + _util.pylatexenc_deprecated_ver( + "1.0", + "The module-level function `pylatexenc.latex2text.latex2text()` is deprecated " + "in favor of the `pylatexenc.latex2text.LatexNodes2Text` class.", + ) + + (nodelist, tpos, tlen) = latexwalker.get_latex_nodes( + content, keep_inline_math=keep_inline_math, tolerant_parsing=tolerant_parsing + ) + + return latexnodes2text( + nodelist, keep_inline_math=keep_inline_math, keep_comments=keep_comments + ) + + +def latexnodes2text(nodelist, keep_inline_math=False, keep_comments=False): + """ + Extracts text from a node list. `nodelist` is a list of nodes as returned by + :py:func:`pylatexenc.latexwalker.get_latex_nodes()`. + + .. deprecated:: 1.0 + Please use :py:class:`LatexNodes2Text` instead. + """ + + _util.pylatexenc_deprecated_ver( + "1.0", + "The module-level function `pylatexenc.latex2text.latexnodes2text()` is " + "deprecated in favor of the `pylatexenc.latex2text.LatexNodes2Text` class.", + ) + + return LatexNodes2Text( + keep_inline_math=keep_inline_math, keep_comments=keep_comments + ).nodelist_to_text(nodelist) diff --git a/good_ai/src/sus/external/pylatexenc/latex2text/__main__.py b/good_ai/src/sus/external/pylatexenc/latex2text/__main__.py new file mode 100644 index 0000000..a19f9b6 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latex2text/__main__.py @@ -0,0 +1,325 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import argparse +import fileinput +import logging +import sys + +from .. import latexwalker +from ..latex2text import LatexNodes2Text, _strict_latex_spaces_predef +from ..version import version_str + + +def main(argv=None): + + if argv is None: + argv = sys.argv[1:] + + parser = argparse.ArgumentParser(prog="latex2text", add_help=False) + + codegroup = parser.add_argument_group("Input options") + + codegroup.add_argument( + "--code", + "-c", + action="store", + default=None, + metavar="LATEX_CODE", + help="Convert the given LATEX_CODE to unicode text instead of reading " + "from FILE or standard input. You cannot specify FILEs if you use this " + "option, and any standard input is ignored.", + ) + + codegroup.add_argument( + "files", + metavar="FILE", + nargs="*", + help="Input files to read LaTeX code from. If no FILE(s) is/are specified, " + "LaTeX code is read from standard input unless --code is specified", + ) + + group = parser.add_argument_group("LatexWalker options") + + group.add_argument( + "--parser-keep-inline-math", + action="store_const", + const=True, + dest="parser_keep_inline_math", + default=None, + help=argparse.SUPPRESS, + ) + group.add_argument( + "--no-parser-keep-inline-math", + action="store_const", + const=False, + dest="parser_keep_inline_math", + help=argparse.SUPPRESS, + ) + + group.add_argument( + "--tolerant-parsing", + action="store_const", + const=True, + dest="tolerant_parsing", + default=True, + ) + group.add_argument( + "--no-tolerant-parsing", + action="store_const", + const=False, + dest="tolerant_parsing", + help="Tolerate syntax errors when parsing, and attempt to continue (default yes)", + ) + + # I'm not sure this flag is useful and if it should be exposed at all. + # Accept it, but make it hidden. + parser.add_argument( + "--strict-braces", + action="store_const", + const=True, + dest="strict_braces", + default=False, + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--no-strict-braces", + action="store_const", + const=False, + dest="strict_braces", + # help="Report errors for mismatching LaTeX braces (default no)" + help=argparse.SUPPRESS, + ) + + group = parser.add_argument_group("LatexNodes2Text options") + + group.add_argument( + "--text-keep-inline-math", + action="store_const", + const=True, + dest="text_keep_inline_math", + default=None, + help=argparse.SUPPRESS, + ) + group.add_argument( + "--no-text-keep-inline-math", + action="store_const", + const=False, + dest="text_keep_inline_math", + help=argparse.SUPPRESS, + ) + + group.add_argument( + "--math-mode", + action="store", + dest="math_mode", + choices=["text", "with-delimiters", "verbatim", "remove"], + default="text", + help="How to handle chunks of math mode LaTeX code. 'text' = convert " + "to text like the rest; 'with-delimiters' = same as 'text' but retain " + "the original math mode delimiters; 'verbatim' = keep verbatim LaTeX code; " + "'remove' = remove from input entirely", + ) + + group.add_argument( + "--fill-text", + dest="fill_text", + action="store", + nargs="?", + default=-1, + help="Attempt to wrap text to the given width, or 80 columns if option is " + "specified with no argument", + ) + + group.add_argument( + "--keep-comments", + action="store_const", + const=True, + dest="keep_comments", + default=False, + ) + group.add_argument( + "--no-keep-comments", + action="store_const", + const=False, + dest="keep_comments", + help="Keep LaTeX comments in text output (default no)", + ) + + class ListWithHiddenItems(list): + def __init__(self, thelist, hiddenitems): + super(ListWithHiddenItems, self).__init__(thelist) + self.hiddenitems = hiddenitems + + def __contains__(self, value): + return ( + super(ListWithHiddenItems, self).__contains__(value) + or value in self.hiddenitems + ) + + strict_latex_spaces_choices = ListWithHiddenItems( + # the list + ["off", "on"] + + list(k for k in _strict_latex_spaces_predef.keys() if k != "default"), + # hidden items: Value is accepted, but not shown in list of choices + ["default"], + ) + group.add_argument( + "--strict-latex-spaces", + choices=strict_latex_spaces_choices, + dest="strict_latex_spaces", + default="macros", + help="How to handle whitespace. See documentation for the class " + "LatexNodes2Text().", + ) + + group.add_argument( + "--keep-braced-groups", + action="store_const", + const=True, + dest="keep_braced_groups", + default=False, + ) + group.add_argument( + "--no-keep-braced-groups", + action="store_const", + const=False, + dest="keep_braced_groups", + help="Keep LaTeX {braced groups} in text output (default no)", + ) + + group.add_argument( + "--keep-braced-groups-minlen", + type=int, + default=2, + dest="keep_braced_groups_minlen", + help="Only apply --keep-braced-groups to groups that contain at least " + "this many characters", + ) + + group = parser.add_argument_group("General options") + + group.add_argument( + "-q", + "--quiet", + dest="logging_level", + action="store_const", + const=logging.ERROR, + default=logging.INFO, + help="Suppress warning messages", + ) + group.add_argument( + "-v", + "--verbose", + dest="logging_level", + action="store_const", + const=logging.DEBUG, + help="Verbose output", + ) + group.add_argument( + "--version", + action="version", + version="pylatexenc {}".format(version_str), + help="Show version information and exit", + ) + group.add_argument( + "--help", action="help", help="Show this help information and exit" + ) + + args = parser.parse_args(argv) + + logging.basicConfig() + logging.getLogger().setLevel(args.logging_level) + logger = logging.getLogger(__name__) + + if ( + args.parser_keep_inline_math is not None + or args.text_keep_inline_math is not None + ): + logger.warning( + "Options --parser-keep-inline-math and --text-keep-inline-math are " + "deprecated and no longer have any effect. Please use " + "--math-mode=... instead." + ) + + latex = "" + if args.code: + if args.files: + logger.error( + "Cannot specify both FILEs and --code option. " + "Use --help option for more information." + ) + sys.exit(1) + latex = args.code + else: + for line in fileinput.input(files=args.files): + latex += line + + if args.fill_text != -1: + if args.fill_text is not None and len(args.fill_text): + fill_text = int(args.fill_text) + else: + fill_text = True + else: + fill_text = None + + lw = latexwalker.LatexWalker( + latex, tolerant_parsing=args.tolerant_parsing, strict_braces=args.strict_braces + ) + + (nodelist, pos, len_) = lw.get_latex_nodes() + + ln2t = LatexNodes2Text( + math_mode=args.math_mode, + keep_comments=args.keep_comments, + strict_latex_spaces=args.strict_latex_spaces, + keep_braced_groups=args.keep_braced_groups, + keep_braced_groups_minlen=args.keep_braced_groups_minlen, + fill_text=fill_text, + ) + + print(ln2t.nodelist_to_text(nodelist)) + + +def run_main(): + + try: + + main() + + except SystemExit: + raise + except: # lgtm [py/catch-base-exception] + import pdb + import traceback + + traceback.print_exc() + pdb.post_mortem() + + +if __name__ == "__main__": + + main() + # run_main() # debug diff --git a/good_ai/src/sus/external/pylatexenc/latex2text/_defaultspecs.py b/good_ai/src/sus/external/pylatexenc/latex2text/_defaultspecs.py new file mode 100644 index 0000000..e4a3e76 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latex2text/_defaultspecs.py @@ -0,0 +1,1784 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2019 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +from __future__ import print_function, unicode_literals # , absolute_import + +import datetime +import sys +import unicodedata + +# Internal module. May change without notice. + + +if sys.version_info.major >= 3: + + def unicode(string): + return string + + basestring = str +else: + pass + + +from ..latex2text import fmt_equation_environment # fmt_placeholder_node, +from ..latex2text import ( + EnvironmentTextSpec, + MacroTextSpec, + SpecialsTextSpec, + fmt_input_macro, + fmt_math_text_style, + fmt_matrix_environment_node, + placeholder_node_formatter, +) + + +def _format_uebung(n, l2tobj): + s = "\n" + l2tobj.nodelist_to_text([n.nodeargs[0]]) + "\n" + optarg = n.nodeargs[1] + if optarg is not None: + s += "[{}]\n".format(l2tobj.nodelist_to_text([optarg])) + return s + + +def _format_maketitle(title, author, date): + s = title + "\n" + s += " " + author + "\n" + s += " " + date + "\n" + s += "=" * max(len(title), 4 + len(author), 4 + len(date)) + "\n\n" + return s + + +def _latex_today(): + return "{dt:%B} {dt.day}, {dt.year}".format(dt=datetime.datetime.now()) + + +def _mathxx_formatter(style): + def formatter(node, l2tobj, style=style): + arg_text = l2tobj.node_arg_to_text(node, 0) + return fmt_math_text_style(arg_text, style) + + return formatter + + +# construct the specs structure, more than the just the following definition + + +# ============================================================================== + + +_latex_specs_placeholders = { + "environments": [ + # --- as of pylatexenc 2.8, these are now approximated --- + # EnvironmentTextSpec('array', simplify_repl=fmt_placeholder_node), + # EnvironmentTextSpec('pmatrix', simplify_repl=fmt_placeholder_node), + # EnvironmentTextSpec('bmatrix', simplify_repl=fmt_placeholder_node), + # EnvironmentTextSpec('smallmatrix', simplify_repl=fmt_placeholder_node), + # EnvironmentTextSpec('psmallmatrix', simplify_repl=fmt_placeholder_node), + # EnvironmentTextSpec('bsmallmatrix', simplify_repl=fmt_placeholder_node), + ], + "specials": [], + "macros": [] + + [ + MacroTextSpec(x, simplify_repl=y) + for x, y in ( + ("includegraphics", placeholder_node_formatter("graphics")), + ("ref", ""), + ("autoref", ""), + ("cref", ""), + ("Cref", ""), + ("eqref", "()"), + ("cite", ""), + ("citet", ""), + ("citep", ""), + ) + ], +} + +_latex_specs_approximations = { + "environments": [ + EnvironmentTextSpec("center", simplify_repl="\n%s\n"), + EnvironmentTextSpec("flushleft", simplify_repl="\n%s\n"), + EnvironmentTextSpec("flushright", simplify_repl="\n%s\n"), + EnvironmentTextSpec("exenumerate", discard=False), + EnvironmentTextSpec("enumerate", discard=False), + EnvironmentTextSpec("list", discard=False), + EnvironmentTextSpec("itemize", discard=False), + EnvironmentTextSpec("subequations", discard=False), + EnvironmentTextSpec("figure", discard=False), + EnvironmentTextSpec("table", discard=False), + EnvironmentTextSpec("array", simplify_repl=fmt_matrix_environment_node), + EnvironmentTextSpec("pmatrix", simplify_repl=fmt_matrix_environment_node), + EnvironmentTextSpec("bmatrix", simplify_repl=fmt_matrix_environment_node), + EnvironmentTextSpec("smallmatrix", simplify_repl=fmt_matrix_environment_node), + EnvironmentTextSpec("psmallmatrix", simplify_repl=fmt_matrix_environment_node), + EnvironmentTextSpec("bsmallmatrix", simplify_repl=fmt_matrix_environment_node), + # + # math environments used to be categorized as 'placeholders' in + # pylatexenc <= 2.9, but I think it's more accurate to have them in + # 'approximations'. + # + EnvironmentTextSpec("equation", simplify_repl=fmt_equation_environment), + # note {equation*} is actually defined by amsmath + EnvironmentTextSpec("equation*", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("eqnarray", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("eqnarray*", simplify_repl=fmt_equation_environment), + # + EnvironmentTextSpec("align", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("multline", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("gather", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("align*", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("multline*", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("gather*", simplify_repl=fmt_equation_environment), + # + # breqn math + EnvironmentTextSpec("dmath", simplify_repl=fmt_equation_environment), + EnvironmentTextSpec("dmath*", simplify_repl=fmt_equation_environment), + ], + "specials": [ + SpecialsTextSpec( + "&", " " + ), # ignore tabular alignments, just add a little space + ], + "macros": [ + # NOTE: macro will only be assigned arguments if they are explicitly + # defined as accepting arguments in the `LatexWalker` (see + # `macrospec` module). + MacroTextSpec("emph", discard=False), + MacroTextSpec("textrm", discard=False), + MacroTextSpec("textit", discard=False), + MacroTextSpec("textbf", discard=False), + MacroTextSpec("textsc", discard=False), + MacroTextSpec("textsl", discard=False), + MacroTextSpec("text", discard=False), + ] + + [ + MacroTextSpec(x, simplify_repl=y) + for x, y in ( + ( + "title", + lambda n, l2tobj: setattr( + l2tobj, + "_doc_title", + l2tobj.nodelist_to_text(n.nodeargd.argnlist[0:1]), + ), + ), + ( + "author", + lambda n, l2tobj: setattr( + l2tobj, + "_doc_author", + l2tobj.nodelist_to_text(n.nodeargd.argnlist[0:1]), + ), + ), + ( + "date", + lambda n, l2tobj: setattr( + l2tobj, + "_doc_date", + l2tobj.nodelist_to_text(n.nodeargd.argnlist[0:1]), + ), + ), + ( + "maketitle", + lambda n, l2tobj: _format_maketitle( + getattr(l2tobj, "_doc_title", r"[NO \title GIVEN]"), + getattr(l2tobj, "_doc_author", r"[NO \author GIVEN]"), + getattr(l2tobj, "_doc_date", _latex_today()), + ), + ), + ("url", "<%s>"), + ( + "item", + lambda r, l2tobj: "\n " + + (l2tobj.nodelist_to_text([r.nodeoptarg]) if r.nodeoptarg else "* "), + ), + ("footnote", "[%(2)s]"), # \footnote[optional mark]{footnote text} + ( + "href", + lambda n, l2tobj: "{} <{}>".format( + l2tobj.nodelist_to_text(n.nodeargd.argnlist[1:1]), + l2tobj.nodelist_to_text(n.nodeargd.argnlist[0:0]), + ), + ), + ( + "part", + lambda n, l2tobj: "\n\nPART: {}\n".format( + l2tobj.node_arg_to_text(n, 2).upper() + ), + ), + ( + "chapter", + lambda n, l2tobj: "\n\nCHAPTER: {}\n".format( + l2tobj.node_arg_to_text(n, 2).upper() + ), + ), + ( + "section", + lambda n, l2tobj: "\n\n\N{SECTION SIGN} {}\n".format( + l2tobj.node_arg_to_text(n, 2).upper() + ), + ), + ( + "subsection", + lambda n, l2tobj: "\n\n \N{SECTION SIGN}.\N{SECTION SIGN} {}\n".format( + l2tobj.node_arg_to_text(n, 2) + ), + ), + ( + "subsubsection", + lambda n, l2tobj: "\n\n \N{SECTION SIGN}.\N{SECTION SIGN}.\N{SECTION SIGN} {}\n".format( + l2tobj.node_arg_to_text(n, 2) + ), + ), + ( + "paragraph", + lambda n, l2tobj: "\n\n {}\n".format(l2tobj.node_arg_to_text(n, 2)), + ), + ( + "subparagraph", + lambda n, l2tobj: "\n\n {}\n".format(l2tobj.node_arg_to_text(n, 2)), + ), + ("textcolor", "%(3)s"), + ("colorbox", "%(3)s"), + ("fcolorbox", "%(5)s"), + ("hspace", ""), + ("vspace", "\n"), + # \\ is treated as an "approximation" because a good text renderer would + # have to actually note that this is a end-of-line marker which is not + # to be confused with other newlines in the paragraph (which can be + # reflowed) + ("\\", "\n"), + ("frac", "%s/%s"), + ("nicefrac", "%s/%s"), + ("textfrac", "%s/%s"), + ("overline", "%s"), + ("underline", "%s"), + ("widehat", "%s"), + ("widetilde", "%s"), + ("wideparen", "%s"), + ("overleftarrow", "%s"), + ("overrightarrow", "%s"), + ("overleftrightarrow", "%s"), + ("underleftarrow", "%s"), + ("underrightarrow", "%s"), + ("underleftrightarrow", "%s"), + ("overbrace", "%s"), + ("underbrace", "%s"), + ("overgroup", "%s"), + ("undergroup", "%s"), + ("overbracket", "%s"), + ("underbracket", "%s"), + ("overlinesegment", "%s"), + ("underlinesegment", "%s"), + ("overleftharpoon", "%s"), + ("overrightharpoon", "%s"), + ) + ], +} + +_latex_specs_base = { + "environments": [], + "specials": [], + "macros": [ + MacroTextSpec("mathrm", discard=False), + MacroTextSpec("mathbf", simplify_repl=_mathxx_formatter("bold")), + MacroTextSpec("mathit", simplify_repl=_mathxx_formatter("italic")), + MacroTextSpec("mathsf", simplify_repl=_mathxx_formatter("sans")), + MacroTextSpec("mathbb", simplify_repl=_mathxx_formatter("doublestruck")), + MacroTextSpec("mathtt", simplify_repl=_mathxx_formatter("monospace")), + MacroTextSpec("mathcal", simplify_repl=_mathxx_formatter("script")), + MacroTextSpec("mathscr", simplify_repl=_mathxx_formatter("script")), + MacroTextSpec("mathfrak", simplify_repl=_mathxx_formatter("fraktur")), + MacroTextSpec("input", simplify_repl=fmt_input_macro), + MacroTextSpec("include", simplify_repl=fmt_input_macro), + ] + + [ + MacroTextSpec(x, simplify_repl=y) + for x, y in ( + ("today", _latex_today()), + # use second argument: + ( + "texorpdfstring", + lambda node, l2tobj: l2tobj.nodelist_to_text(node.nodeargs[1:2]), + ), + ("oe", "\u0153"), + ("OE", "\u0152"), + ("ae", "\u00e6"), + ("AE", "\u00c6"), + ("aa", "\u00e5"), # a norvegien/nordique + ("AA", "\u00c5"), # A norvegien/nordique + ("o", "\u00f8"), # o norvegien/nordique + ("O", "\u00d8"), # O norvegien/nordique + ("ss", "\u00df"), # s-z allemand + ("L", "\N{LATIN CAPITAL LETTER L WITH STROKE}"), + ("l", "\N{LATIN SMALL LETTER L WITH STROKE}"), + ("i", "\N{LATIN SMALL LETTER DOTLESS I}"), + ("j", "\N{LATIN SMALL LETTER DOTLESS J}"), + ("~", "~"), + ("&", "&"), + ("$", "$"), + ("{", "{"), + ("}", "}"), + ("%", lambda arg: "%"), # careful: % is formatting substitution symbol... + ("#", "#"), + ("_", "_"), + ("textquoteleft", "\N{LEFT SINGLE QUOTATION MARK}"), + ("textquoteright", "\N{RIGHT SINGLE QUOTATION MARK}"), + ("textquotedblright", "\N{RIGHT DOUBLE QUOTATION MARK}"), + ("textquotedblleft", "\N{LEFT DOUBLE QUOTATION MARK}"), + ("textendash", "\N{EN DASH}"), + ("textemdash", "\N{EM DASH}"), + ("textpm", "\N{PLUS-MINUS SIGN}"), + ("textmp", "\N{MINUS-OR-PLUS SIGN}"), + ("texteuro", "\N{EURO SIGN}"), + ("backslash", "\\"), + ("textbackslash", "\\"), + # math stuff + ("hbar", "\N{LATIN SMALL LETTER H WITH STROKE}"), + ("ell", "\N{SCRIPT SMALL L}"), + ("forall", "\N{FOR ALL}"), + ("complement", "\N{COMPLEMENT}"), + ("partial", "\N{PARTIAL DIFFERENTIAL}"), + ("exists", "\N{THERE EXISTS}"), + ("nexists", "\N{THERE DOES NOT EXIST}"), + ("varnothing", "\N{EMPTY SET}"), + ("emptyset", "\N{EMPTY SET}"), + ("aleph", "\N{ALEF SYMBOL}"), + # increment? + ("nabla", "\N{NABLA}"), + # + ("in", "\N{ELEMENT OF}"), + ("notin", "\N{NOT AN ELEMENT OF}"), + ("ni", "\N{CONTAINS AS MEMBER}"), + ("prod", "\N{N-ARY PRODUCT}"), + ("coprod", "\N{N-ARY COPRODUCT}"), + ("sum", "\N{N-ARY SUMMATION}"), + ("setminus", "\N{SET MINUS}"), + ("smallsetminus", "\N{SET MINUS}"), + ("ast", "\N{ASTERISK OPERATOR}"), + ("circ", "\N{RING OPERATOR}"), + ("bullet", "\N{BULLET OPERATOR}"), + ("sqrt", "\N{SQUARE ROOT}(%(2)s)"), + ("propto", "\N{PROPORTIONAL TO}"), + ("infty", "\N{INFINITY}"), + ("parallel", "\N{PARALLEL TO}"), + ("nparallel", "\N{NOT PARALLEL TO}"), + ("wedge", "\N{LOGICAL AND}"), + ("vee", "\N{LOGICAL OR}"), + ("cap", "\N{INTERSECTION}"), + ("cup", "\N{UNION}"), + ("int", "\N{INTEGRAL}"), + ("iint", "\N{DOUBLE INTEGRAL}"), + ("iiint", "\N{TRIPLE INTEGRAL}"), + ("oint", "\N{CONTOUR INTEGRAL}"), + ("sim", "\N{TILDE OPERATOR}"), + ("backsim", "\N{REVERSED TILDE}"), + ("simeq", "\N{ASYMPTOTICALLY EQUAL TO}"), + ("approx", "\N{ALMOST EQUAL TO}"), + ("neq", "\N{NOT EQUAL TO}"), + ("equiv", "\N{IDENTICAL TO}"), + ("le", "\N{LESS-THAN OR EQUAL TO}"), + ("ge", "\N{GREATER-THAN OR EQUAL TO}"), + ("leq", "\N{LESS-THAN OR EQUAL TO}"), + ("geq", "\N{GREATER-THAN OR EQUAL TO}"), + ("leqslant", "\N{LESS-THAN OR SLANTED EQUAL TO}"), + ("geqslant", "\N{GREATER-THAN OR SLANTED EQUAL TO}"), + ("leqq", "\N{LESS-THAN OVER EQUAL TO}"), + ("geqq", "\N{GREATER-THAN OVER EQUAL TO}"), + ("lneqq", "\N{LESS-THAN BUT NOT EQUAL TO}"), + ("gneqq", "\N{GREATER-THAN BUT NOT EQUAL TO}"), + ("ll", "\N{MUCH LESS-THAN}"), + ("gg", "\N{MUCH GREATER-THAN}"), + ("nless", "\N{NOT LESS-THAN}"), + ("ngtr", "\N{NOT GREATER-THAN}"), + ("nleq", "\N{NEITHER LESS-THAN NOR EQUAL TO}"), + ("ngeq", "\N{NEITHER GREATER-THAN NOR EQUAL TO}"), + ("lesssim", "\N{LESS-THAN OR EQUIVALENT TO}"), + ("gtrsim", "\N{GREATER-THAN OR EQUIVALENT TO}"), + ("lessgtr", "\N{LESS-THAN OR GREATER-THAN}"), + ("gtrless", "\N{GREATER-THAN OR LESS-THAN}"), + ("prec", "\N{PRECEDES}"), + ("succ", "\N{SUCCEEDS}"), + ("preceq", "\N{PRECEDES OR EQUAL TO}"), + ("succeq", "\N{SUCCEEDS OR EQUAL TO}"), + ("precsim", "\N{PRECEDES OR EQUIVALENT TO}"), + ("succsim", "\N{SUCCEEDS OR EQUIVALENT TO}"), + ("nprec", "\N{DOES NOT PRECEDE}"), + ("nsucc", "\N{DOES NOT SUCCEED}"), + ("subset", "\N{SUBSET OF}"), + ("supset", "\N{SUPERSET OF}"), + ("subseteq", "\N{SUBSET OF OR EQUAL TO}"), + ("supseteq", "\N{SUPERSET OF OR EQUAL TO}"), + ("nsubseteq", "\N{NEITHER A SUBSET OF NOR EQUAL TO}"), + ("nsupseteq", "\N{NEITHER A SUPERSET OF NOR EQUAL TO}"), + ("subsetneq", "\N{SUBSET OF WITH NOT EQUAL TO}"), + ("supsetneq", "\N{SUPERSET OF WITH NOT EQUAL TO}"), + ("cdot", "\N{MIDDLE DOT}"), + ("times", "\N{MULTIPLICATION SIGN}"), + ("otimes", "\N{CIRCLED TIMES}"), + ("oplus", "\N{CIRCLED PLUS}"), + ("bigotimes", "\N{CIRCLED TIMES}"), + ("bigoplus", "\N{CIRCLED PLUS}"), + ("cos", "cos"), + ("sin", "sin"), + ("tan", "tan"), + ("arccos", "arccos"), + ("arcsin", "arcsin"), + ("arctan", "arctan"), + ("cosh", "cosh"), + ("sinh", "sinh"), + ("tanh", "tanh"), + ("arccosh", "arccosh"), + ("arcsinh", "arcsinh"), + ("arctanh", "arctanh"), + ("ln", "ln"), + ("log", "log"), + ("exp", "exp"), + ("max", "max"), + ("min", "min"), + ("sup", "sup"), + ("inf", "inf"), + ("lim", "lim"), + ("limsup", "lim sup"), + ("liminf", "lim inf"), + ("prime", "'"), + ("dag", "\N{DAGGER}"), + ("dagger", "\N{DAGGER}"), + ("pm", "\N{PLUS-MINUS SIGN}"), + ("mp", "\N{MINUS-OR-PLUS SIGN}"), + (",", " "), + (";", " "), + (":", " "), + (" ", " "), + ("!", ""), # sorry, no negative space in ascii + ("quad", " "), + ("qquad", " "), + ("ldots", "\N{HORIZONTAL ELLIPSIS}"), + ("cdots", "\N{MIDLINE HORIZONTAL ELLIPSIS}"), + ("ddots", "\N{DOWN RIGHT DIAGONAL ELLIPSIS}"), + ("iddots", "\N{UP RIGHT DIAGONAL ELLIPSIS}"), + ("vdots", "\N{VERTICAL ELLIPSIS}"), + ("dots", "\N{HORIZONTAL ELLIPSIS}"), + ("dotsc", "\N{HORIZONTAL ELLIPSIS}"), + ("dotsb", "\N{HORIZONTAL ELLIPSIS}"), + ("dotsm", "\N{HORIZONTAL ELLIPSIS}"), + ("dotsi", "\N{HORIZONTAL ELLIPSIS}"), + ("dotso", "\N{HORIZONTAL ELLIPSIS}"), + ("langle", "\N{MATHEMATICAL LEFT ANGLE BRACKET}"), + ("rangle", "\N{MATHEMATICAL RIGHT ANGLE BRACKET}"), + ("lvert", "|"), + ("rvert", "|"), + ("vert", "|"), + ("lVert", "\N{DOUBLE VERTICAL LINE}"), + ("rVert", "\N{DOUBLE VERTICAL LINE}"), + ("Vert", "\N{DOUBLE VERTICAL LINE}"), + ("mid", "|"), + ("nmid", "\N{DOES NOT DIVIDE}"), + ("ket", "|%s\N{MATHEMATICAL RIGHT ANGLE BRACKET}"), + ("bra", "\N{MATHEMATICAL LEFT ANGLE BRACKET}%s|"), + ( + "braket", + "\N{MATHEMATICAL LEFT ANGLE BRACKET}%s|%s\N{MATHEMATICAL RIGHT ANGLE BRACKET}", + ), + ( + "ketbra", + "|%s\N{MATHEMATICAL RIGHT ANGLE BRACKET}\N{MATHEMATICAL LEFT ANGLE BRACKET}%s|", + ), + ("uparrow", "\N{UPWARDS ARROW}"), + ("downarrow", "\N{DOWNWARDS ARROW}"), + ("rightarrow", "\N{RIGHTWARDS ARROW}"), + ("to", "\N{RIGHTWARDS ARROW}"), + ("leftarrow", "\N{LEFTWARDS ARROW}"), + ("longrightarrow", "\N{LONG RIGHTWARDS ARROW}"), + ("longleftarrow", "\N{LONG LEFTWARDS ARROW}"), + ) + ], +} + + +# ============================================================================== + +advanced_symbols_macros = [ + # Rules from latexencode defaults 'defaults' + MacroTextSpec("textasciicircum", "\N{CIRCUMFLEX ACCENT}"), # ‘^’ + MacroTextSpec("textasciitilde", "\N{TILDE}"), # ‘~’ + MacroTextSpec("textexclamdown", "\N{INVERTED EXCLAMATION MARK}"), # ‘¡’ + MacroTextSpec("textcent", "\N{CENT SIGN}"), # ‘¢’ + MacroTextSpec("textsterling", "\N{POUND SIGN}"), # ‘£’ + MacroTextSpec("textcurrency", "\N{CURRENCY SIGN}"), # ‘¤’ + MacroTextSpec("textyen", "\N{YEN SIGN}"), # ‘¥’ + MacroTextSpec("textbrokenbar", "\N{BROKEN BAR}"), # ‘¦’ + MacroTextSpec("textsection", "\N{SECTION SIGN}"), # ‘§’ + MacroTextSpec("textasciidieresis", "\N{DIAERESIS}"), # ‘¨’ + MacroTextSpec("textcopyright", "\N{COPYRIGHT SIGN}"), # ‘©’ + MacroTextSpec("textordfeminine", "\N{FEMININE ORDINAL INDICATOR}"), # ‘ª’ + MacroTextSpec( + "guillemotleft", "\N{LEFT-POINTING DOUBLE ANGLE QUOTATION MARK}" + ), # ‘«’ + MacroTextSpec("textlnot", "\N{NOT SIGN}"), # ‘¬’ + MacroTextSpec("-", "\N{SOFT HYPHEN}"), # ‘­’ + MacroTextSpec("textregistered", "\N{REGISTERED SIGN}"), # ‘®’ + MacroTextSpec("textasciimacron", "\N{MACRON}"), # ‘¯’ + MacroTextSpec("textdegree", "\N{DEGREE SIGN}"), # ‘°’ + MacroTextSpec("texttwosuperior", "\N{SUPERSCRIPT TWO}"), # ‘²’ + MacroTextSpec("textthreesuperior", "\N{SUPERSCRIPT THREE}"), # ‘³’ + MacroTextSpec("textasciiacute", "\N{ACUTE ACCENT}"), # ‘´’ + MacroTextSpec("textmu", "\N{MICRO SIGN}"), # ‘µ’ + MacroTextSpec("textparagraph", "\N{PILCROW SIGN}"), # ‘¶’ + MacroTextSpec("textperiodcentered", "\N{MIDDLE DOT}"), # ‘·’ + MacroTextSpec("textonesuperior", "\N{SUPERSCRIPT ONE}"), # ‘¹’ + MacroTextSpec("textordmasculine", "\N{MASCULINE ORDINAL INDICATOR}"), # ‘º’ + MacroTextSpec( + "guillemotright", "\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}" + ), # ‘»’ + MacroTextSpec("textonequarter", "\N{VULGAR FRACTION ONE QUARTER}"), # ‘¼’ + MacroTextSpec("textonehalf", "\N{VULGAR FRACTION ONE HALF}"), # ‘½’ + MacroTextSpec("textthreequarters", "\N{VULGAR FRACTION THREE QUARTERS}"), # ‘¾’ + MacroTextSpec("textquestiondown", "\N{INVERTED QUESTION MARK}"), # ‘¿’ + MacroTextSpec("DH", "\N{LATIN CAPITAL LETTER ETH}"), # ‘Ð’ + MacroTextSpec("texttimes", "\N{MULTIPLICATION SIGN}"), # ‘×’ + MacroTextSpec("TH", "\N{LATIN CAPITAL LETTER THORN}"), # ‘Þ’ + MacroTextSpec("dh", "\N{LATIN SMALL LETTER ETH}"), # ‘ð’ + MacroTextSpec("textdiv", "\N{DIVISION SIGN}"), # ‘÷’ + MacroTextSpec("th", "\N{LATIN SMALL LETTER THORN}"), # ‘þ’ + MacroTextSpec("DJ", "\N{LATIN CAPITAL LETTER D WITH STROKE}"), # ‘Đ’ + MacroTextSpec("dj", "\N{LATIN SMALL LETTER D WITH STROKE}"), # ‘đ’ + MacroTextSpec("IJ", "\N{LATIN CAPITAL LIGATURE IJ}"), # ‘IJ’ + MacroTextSpec("ij", "\N{LATIN SMALL LIGATURE IJ}"), # ‘ij’ + MacroTextSpec("NG", "\N{LATIN CAPITAL LETTER ENG}"), # ‘Ŋ’ + MacroTextSpec("ng", "\N{LATIN SMALL LETTER ENG}"), # ‘ŋ’ + MacroTextSpec("textflorin", "\N{LATIN SMALL LETTER F WITH HOOK}"), # ‘ƒ’ + MacroTextSpec("texthvlig", "\N{LATIN SMALL LETTER HV}"), # ‘ƕ’ + MacroTextSpec("textnrleg", "\N{LATIN SMALL LETTER N WITH LONG RIGHT LEG}"), # ‘ƞ’ + MacroTextSpec("textschwa", "\N{LATIN SMALL LETTER SCHWA}"), # ‘ə’ + MacroTextSpec("textphi", "\N{LATIN SMALL LETTER PHI}"), # ‘ɸ’ + MacroTextSpec("textglotstop", "\N{LATIN LETTER GLOTTAL STOP}"), # ‘ʔ’ + MacroTextSpec("textturnk", "\N{LATIN SMALL LETTER TURNED K}"), # ‘ʞ’ + MacroTextSpec("textasciicircum", "\N{MODIFIER LETTER CIRCUMFLEX ACCENT}"), # ‘ˆ’ + MacroTextSpec("textasciicaron", "\N{CARON}"), # ‘ˇ’ + MacroTextSpec("textasciibreve", "\N{BREVE}"), # ‘˘’ + MacroTextSpec("textperiodcentered", "\N{DOT ABOVE}"), # ‘˙’ + MacroTextSpec("textasciitilde", "\N{SMALL TILDE}"), # ‘˜’ + MacroTextSpec("textacutedbl", "\N{DOUBLE ACUTE ACCENT}"), # ‘˝’ + MacroTextSpec("varkappa", "\N{GREEK KAPPA SYMBOL}"), # ‘ϰ’ + MacroTextSpec("backepsilon", "\N{GREEK REVERSED LUNATE EPSILON SYMBOL}"), # ‘϶’ + MacroTextSpec("CYRYO", "\N{CYRILLIC CAPITAL LETTER IO}"), # ‘Ё’ + MacroTextSpec("CYRDJE", "\N{CYRILLIC CAPITAL LETTER DJE}"), # ‘Ђ’ + MacroTextSpec("CYRIE", "\N{CYRILLIC CAPITAL LETTER UKRAINIAN IE}"), # ‘Є’ + MacroTextSpec("CYRDZE", "\N{CYRILLIC CAPITAL LETTER DZE}"), # ‘Ѕ’ + MacroTextSpec( + "CYRII", "\N{CYRILLIC CAPITAL LETTER BYELORUSSIAN-UKRAINIAN I}" + ), # ‘І’ + MacroTextSpec("CYRYI", "\N{CYRILLIC CAPITAL LETTER YI}"), # ‘Ї’ + MacroTextSpec("CYRJE", "\N{CYRILLIC CAPITAL LETTER JE}"), # ‘Ј’ + MacroTextSpec("CYRLJE", "\N{CYRILLIC CAPITAL LETTER LJE}"), # ‘Љ’ + MacroTextSpec("CYRNJE", "\N{CYRILLIC CAPITAL LETTER NJE}"), # ‘Њ’ + MacroTextSpec("CYRTSHE", "\N{CYRILLIC CAPITAL LETTER TSHE}"), # ‘Ћ’ + MacroTextSpec("CYRUSHRT", "\N{CYRILLIC CAPITAL LETTER SHORT U}"), # ‘Ў’ + MacroTextSpec("CYRDZHE", "\N{CYRILLIC CAPITAL LETTER DZHE}"), # ‘Џ’ + MacroTextSpec("CYRA", "\N{CYRILLIC CAPITAL LETTER A}"), # ‘А’ + MacroTextSpec("CYRB", "\N{CYRILLIC CAPITAL LETTER BE}"), # ‘Б’ + MacroTextSpec("CYRV", "\N{CYRILLIC CAPITAL LETTER VE}"), # ‘В’ + MacroTextSpec("CYRG", "\N{CYRILLIC CAPITAL LETTER GHE}"), # ‘Г’ + MacroTextSpec("CYRD", "\N{CYRILLIC CAPITAL LETTER DE}"), # ‘Д’ + MacroTextSpec("CYRE", "\N{CYRILLIC CAPITAL LETTER IE}"), # ‘Е’ + MacroTextSpec("CYRZH", "\N{CYRILLIC CAPITAL LETTER ZHE}"), # ‘Ж’ + MacroTextSpec("CYRZ", "\N{CYRILLIC CAPITAL LETTER ZE}"), # ‘З’ + MacroTextSpec("CYRI", "\N{CYRILLIC CAPITAL LETTER I}"), # ‘И’ + MacroTextSpec("CYRISHRT", "\N{CYRILLIC CAPITAL LETTER SHORT I}"), # ‘Й’ + MacroTextSpec("CYRK", "\N{CYRILLIC CAPITAL LETTER KA}"), # ‘К’ + MacroTextSpec("CYRL", "\N{CYRILLIC CAPITAL LETTER EL}"), # ‘Л’ + MacroTextSpec("CYRM", "\N{CYRILLIC CAPITAL LETTER EM}"), # ‘М’ + MacroTextSpec("CYRN", "\N{CYRILLIC CAPITAL LETTER EN}"), # ‘Н’ + MacroTextSpec("CYRO", "\N{CYRILLIC CAPITAL LETTER O}"), # ‘О’ + MacroTextSpec("CYRP", "\N{CYRILLIC CAPITAL LETTER PE}"), # ‘П’ + MacroTextSpec("CYRR", "\N{CYRILLIC CAPITAL LETTER ER}"), # ‘Р’ + MacroTextSpec("CYRS", "\N{CYRILLIC CAPITAL LETTER ES}"), # ‘С’ + MacroTextSpec("CYRT", "\N{CYRILLIC CAPITAL LETTER TE}"), # ‘Т’ + MacroTextSpec("CYRU", "\N{CYRILLIC CAPITAL LETTER U}"), # ‘У’ + MacroTextSpec("CYRF", "\N{CYRILLIC CAPITAL LETTER EF}"), # ‘Ф’ + MacroTextSpec("CYRH", "\N{CYRILLIC CAPITAL LETTER HA}"), # ‘Х’ + MacroTextSpec("CYRC", "\N{CYRILLIC CAPITAL LETTER TSE}"), # ‘Ц’ + MacroTextSpec("CYRCH", "\N{CYRILLIC CAPITAL LETTER CHE}"), # ‘Ч’ + MacroTextSpec("CYRSH", "\N{CYRILLIC CAPITAL LETTER SHA}"), # ‘Ш’ + MacroTextSpec("CYRSHCH", "\N{CYRILLIC CAPITAL LETTER SHCHA}"), # ‘Щ’ + MacroTextSpec("CYRHRDSN", "\N{CYRILLIC CAPITAL LETTER HARD SIGN}"), # ‘Ъ’ + MacroTextSpec("CYRERY", "\N{CYRILLIC CAPITAL LETTER YERU}"), # ‘Ы’ + MacroTextSpec("CYRSFTSN", "\N{CYRILLIC CAPITAL LETTER SOFT SIGN}"), # ‘Ь’ + MacroTextSpec("CYREREV", "\N{CYRILLIC CAPITAL LETTER E}"), # ‘Э’ + MacroTextSpec("CYRYU", "\N{CYRILLIC CAPITAL LETTER YU}"), # ‘Ю’ + MacroTextSpec("CYRYA", "\N{CYRILLIC CAPITAL LETTER YA}"), # ‘Я’ + MacroTextSpec("cyra", "\N{CYRILLIC SMALL LETTER A}"), # ‘а’ + MacroTextSpec("cyrb", "\N{CYRILLIC SMALL LETTER BE}"), # ‘б’ + MacroTextSpec("cyrv", "\N{CYRILLIC SMALL LETTER VE}"), # ‘в’ + MacroTextSpec("cyrg", "\N{CYRILLIC SMALL LETTER GHE}"), # ‘г’ + MacroTextSpec("cyrd", "\N{CYRILLIC SMALL LETTER DE}"), # ‘д’ + MacroTextSpec("cyre", "\N{CYRILLIC SMALL LETTER IE}"), # ‘е’ + MacroTextSpec("cyrzh", "\N{CYRILLIC SMALL LETTER ZHE}"), # ‘ж’ + MacroTextSpec("cyrz", "\N{CYRILLIC SMALL LETTER ZE}"), # ‘з’ + MacroTextSpec("cyri", "\N{CYRILLIC SMALL LETTER I}"), # ‘и’ + MacroTextSpec("cyrishrt", "\N{CYRILLIC SMALL LETTER SHORT I}"), # ‘й’ + MacroTextSpec("cyrk", "\N{CYRILLIC SMALL LETTER KA}"), # ‘к’ + MacroTextSpec("cyrl", "\N{CYRILLIC SMALL LETTER EL}"), # ‘л’ + MacroTextSpec("cyrm", "\N{CYRILLIC SMALL LETTER EM}"), # ‘м’ + MacroTextSpec("cyrn", "\N{CYRILLIC SMALL LETTER EN}"), # ‘н’ + MacroTextSpec("cyro", "\N{CYRILLIC SMALL LETTER O}"), # ‘о’ + MacroTextSpec("cyrp", "\N{CYRILLIC SMALL LETTER PE}"), # ‘п’ + MacroTextSpec("cyrr", "\N{CYRILLIC SMALL LETTER ER}"), # ‘р’ + MacroTextSpec("cyrs", "\N{CYRILLIC SMALL LETTER ES}"), # ‘с’ + MacroTextSpec("cyrt", "\N{CYRILLIC SMALL LETTER TE}"), # ‘т’ + MacroTextSpec("cyru", "\N{CYRILLIC SMALL LETTER U}"), # ‘у’ + MacroTextSpec("cyrf", "\N{CYRILLIC SMALL LETTER EF}"), # ‘ф’ + MacroTextSpec("cyrh", "\N{CYRILLIC SMALL LETTER HA}"), # ‘х’ + MacroTextSpec("cyrc", "\N{CYRILLIC SMALL LETTER TSE}"), # ‘ц’ + MacroTextSpec("cyrch", "\N{CYRILLIC SMALL LETTER CHE}"), # ‘ч’ + MacroTextSpec("cyrsh", "\N{CYRILLIC SMALL LETTER SHA}"), # ‘ш’ + MacroTextSpec("cyrshch", "\N{CYRILLIC SMALL LETTER SHCHA}"), # ‘щ’ + MacroTextSpec("cyrhrdsn", "\N{CYRILLIC SMALL LETTER HARD SIGN}"), # ‘ъ’ + MacroTextSpec("cyrery", "\N{CYRILLIC SMALL LETTER YERU}"), # ‘ы’ + MacroTextSpec("cyrsftsn", "\N{CYRILLIC SMALL LETTER SOFT SIGN}"), # ‘ь’ + MacroTextSpec("cyrerev", "\N{CYRILLIC SMALL LETTER E}"), # ‘э’ + MacroTextSpec("cyryu", "\N{CYRILLIC SMALL LETTER YU}"), # ‘ю’ + MacroTextSpec("cyrya", "\N{CYRILLIC SMALL LETTER YA}"), # ‘я’ + MacroTextSpec("cyryo", "\N{CYRILLIC SMALL LETTER IO}"), # ‘ё’ + MacroTextSpec("cyrdje", "\N{CYRILLIC SMALL LETTER DJE}"), # ‘ђ’ + MacroTextSpec("cyrie", "\N{CYRILLIC SMALL LETTER UKRAINIAN IE}"), # ‘є’ + MacroTextSpec("cyrdze", "\N{CYRILLIC SMALL LETTER DZE}"), # ‘ѕ’ + MacroTextSpec("cyrii", "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}"), # ‘і’ + MacroTextSpec("cyryi", "\N{CYRILLIC SMALL LETTER YI}"), # ‘ї’ + MacroTextSpec("cyrje", "\N{CYRILLIC SMALL LETTER JE}"), # ‘ј’ + MacroTextSpec("cyrlje", "\N{CYRILLIC SMALL LETTER LJE}"), # ‘љ’ + MacroTextSpec("cyrnje", "\N{CYRILLIC SMALL LETTER NJE}"), # ‘њ’ + MacroTextSpec("cyrtshe", "\N{CYRILLIC SMALL LETTER TSHE}"), # ‘ћ’ + MacroTextSpec("cyrushrt", "\N{CYRILLIC SMALL LETTER SHORT U}"), # ‘ў’ + MacroTextSpec("cyrdzhe", "\N{CYRILLIC SMALL LETTER DZHE}"), # ‘џ’ + MacroTextSpec("CYRYAT", "\N{CYRILLIC CAPITAL LETTER YAT}"), # ‘Ѣ’ + MacroTextSpec("cyryat", "\N{CYRILLIC SMALL LETTER YAT}"), # ‘ѣ’ + MacroTextSpec("CYRBYUS", "\N{CYRILLIC CAPITAL LETTER BIG YUS}"), # ‘Ѫ’ + MacroTextSpec("cyrbyus", "\N{CYRILLIC SMALL LETTER BIG YUS}"), # ‘ѫ’ + MacroTextSpec("CYRFITA", "\N{CYRILLIC CAPITAL LETTER FITA}"), # ‘Ѳ’ + MacroTextSpec("cyrfita", "\N{CYRILLIC SMALL LETTER FITA}"), # ‘ѳ’ + MacroTextSpec("CYRIZH", "\N{CYRILLIC CAPITAL LETTER IZHITSA}"), # ‘Ѵ’ + MacroTextSpec("cyrizh", "\N{CYRILLIC SMALL LETTER IZHITSA}"), # ‘ѵ’ + MacroTextSpec("CYRSEMISFTSN", "\N{CYRILLIC CAPITAL LETTER SEMISOFT SIGN}"), # ‘Ҍ’ + MacroTextSpec("cyrsemisftsn", "\N{CYRILLIC SMALL LETTER SEMISOFT SIGN}"), # ‘ҍ’ + MacroTextSpec("CYRRTICK", "\N{CYRILLIC CAPITAL LETTER ER WITH TICK}"), # ‘Ҏ’ + MacroTextSpec("cyrrtick", "\N{CYRILLIC SMALL LETTER ER WITH TICK}"), # ‘ҏ’ + MacroTextSpec("CYRGUP", "\N{CYRILLIC CAPITAL LETTER GHE WITH UPTURN}"), # ‘Ґ’ + MacroTextSpec("cyrgup", "\N{CYRILLIC SMALL LETTER GHE WITH UPTURN}"), # ‘ґ’ + MacroTextSpec("CYRGHCRS", "\N{CYRILLIC CAPITAL LETTER GHE WITH STROKE}"), # ‘Ғ’ + MacroTextSpec("cyrghcrs", "\N{CYRILLIC SMALL LETTER GHE WITH STROKE}"), # ‘ғ’ + MacroTextSpec("CYRGHK", "\N{CYRILLIC CAPITAL LETTER GHE WITH MIDDLE HOOK}"), # ‘Ҕ’ + MacroTextSpec("cyrghk", "\N{CYRILLIC SMALL LETTER GHE WITH MIDDLE HOOK}"), # ‘ҕ’ + MacroTextSpec("CYRZHDSC", "\N{CYRILLIC CAPITAL LETTER ZHE WITH DESCENDER}"), # ‘Җ’ + MacroTextSpec("cyrzhdsc", "\N{CYRILLIC SMALL LETTER ZHE WITH DESCENDER}"), # ‘җ’ + MacroTextSpec("CYRZDSC", "\N{CYRILLIC CAPITAL LETTER ZE WITH DESCENDER}"), # ‘Ҙ’ + MacroTextSpec("cyrzdsc", "\N{CYRILLIC SMALL LETTER ZE WITH DESCENDER}"), # ‘ҙ’ + MacroTextSpec("CYRKDSC", "\N{CYRILLIC CAPITAL LETTER KA WITH DESCENDER}"), # ‘Қ’ + MacroTextSpec("cyrkdsc", "\N{CYRILLIC SMALL LETTER KA WITH DESCENDER}"), # ‘қ’ + MacroTextSpec( + "CYRKVCRS", "\N{CYRILLIC CAPITAL LETTER KA WITH VERTICAL STROKE}" + ), # ‘Ҝ’ + MacroTextSpec( + "cyrkvcrs", "\N{CYRILLIC SMALL LETTER KA WITH VERTICAL STROKE}" + ), # ‘ҝ’ + MacroTextSpec("CYRKHCRS", "\N{CYRILLIC CAPITAL LETTER KA WITH STROKE}"), # ‘Ҟ’ + MacroTextSpec("cyrkhcrs", "\N{CYRILLIC SMALL LETTER KA WITH STROKE}"), # ‘ҟ’ + MacroTextSpec("CYRKBEAK", "\N{CYRILLIC CAPITAL LETTER BASHKIR KA}"), # ‘Ҡ’ + MacroTextSpec("cyrkbeak", "\N{CYRILLIC SMALL LETTER BASHKIR KA}"), # ‘ҡ’ + MacroTextSpec("CYRNDSC", "\N{CYRILLIC CAPITAL LETTER EN WITH DESCENDER}"), # ‘Ң’ + MacroTextSpec("cyrndsc", "\N{CYRILLIC SMALL LETTER EN WITH DESCENDER}"), # ‘ң’ + MacroTextSpec("CYRNG", "\N{CYRILLIC CAPITAL LIGATURE EN GHE}"), # ‘Ҥ’ + MacroTextSpec("cyrng", "\N{CYRILLIC SMALL LIGATURE EN GHE}"), # ‘ҥ’ + MacroTextSpec("CYRPHK", "\N{CYRILLIC CAPITAL LETTER PE WITH MIDDLE HOOK}"), # ‘Ҧ’ + MacroTextSpec("cyrphk", "\N{CYRILLIC SMALL LETTER PE WITH MIDDLE HOOK}"), # ‘ҧ’ + MacroTextSpec("CYRABHHA", "\N{CYRILLIC CAPITAL LETTER ABKHASIAN HA}"), # ‘Ҩ’ + MacroTextSpec("cyrabhha", "\N{CYRILLIC SMALL LETTER ABKHASIAN HA}"), # ‘ҩ’ + MacroTextSpec("CYRSDSC", "\N{CYRILLIC CAPITAL LETTER ES WITH DESCENDER}"), # ‘Ҫ’ + MacroTextSpec("cyrsdsc", "\N{CYRILLIC SMALL LETTER ES WITH DESCENDER}"), # ‘ҫ’ + MacroTextSpec("CYRTDSC", "\N{CYRILLIC CAPITAL LETTER TE WITH DESCENDER}"), # ‘Ҭ’ + MacroTextSpec("cyrtdsc", "\N{CYRILLIC SMALL LETTER TE WITH DESCENDER}"), # ‘ҭ’ + MacroTextSpec("CYRY", "\N{CYRILLIC CAPITAL LETTER STRAIGHT U}"), # ‘Ү’ + MacroTextSpec("cyry", "\N{CYRILLIC SMALL LETTER STRAIGHT U}"), # ‘ү’ + MacroTextSpec( + "CYRYHCRS", "\N{CYRILLIC CAPITAL LETTER STRAIGHT U WITH STROKE}" + ), # ‘Ұ’ + MacroTextSpec( + "cyryhcrs", "\N{CYRILLIC SMALL LETTER STRAIGHT U WITH STROKE}" + ), # ‘ұ’ + MacroTextSpec("CYRHDSC", "\N{CYRILLIC CAPITAL LETTER HA WITH DESCENDER}"), # ‘Ҳ’ + MacroTextSpec("cyrhdsc", "\N{CYRILLIC SMALL LETTER HA WITH DESCENDER}"), # ‘ҳ’ + MacroTextSpec("CYRTETSE", "\N{CYRILLIC CAPITAL LIGATURE TE TSE}"), # ‘Ҵ’ + MacroTextSpec("cyrtetse", "\N{CYRILLIC SMALL LIGATURE TE TSE}"), # ‘ҵ’ + MacroTextSpec("CYRCHRDSC", "\N{CYRILLIC CAPITAL LETTER CHE WITH DESCENDER}"), # ‘Ҷ’ + MacroTextSpec("cyrchrdsc", "\N{CYRILLIC SMALL LETTER CHE WITH DESCENDER}"), # ‘ҷ’ + MacroTextSpec( + "CYRCHVCRS", "\N{CYRILLIC CAPITAL LETTER CHE WITH VERTICAL STROKE}" + ), # ‘Ҹ’ + MacroTextSpec( + "cyrchvcrs", "\N{CYRILLIC SMALL LETTER CHE WITH VERTICAL STROKE}" + ), # ‘ҹ’ + MacroTextSpec("CYRSHHA", "\N{CYRILLIC CAPITAL LETTER SHHA}"), # ‘Һ’ + MacroTextSpec("cyrshha", "\N{CYRILLIC SMALL LETTER SHHA}"), # ‘һ’ + MacroTextSpec("CYRABHCH", "\N{CYRILLIC CAPITAL LETTER ABKHASIAN CHE}"), # ‘Ҽ’ + MacroTextSpec("cyrabhch", "\N{CYRILLIC SMALL LETTER ABKHASIAN CHE}"), # ‘ҽ’ + MacroTextSpec( + "CYRABHCHDSC", "\N{CYRILLIC CAPITAL LETTER ABKHASIAN CHE WITH DESCENDER}" + ), # ‘Ҿ’ + MacroTextSpec( + "cyrabhchdsc", "\N{CYRILLIC SMALL LETTER ABKHASIAN CHE WITH DESCENDER}" + ), # ‘ҿ’ + MacroTextSpec("CYRpalochka", "\N{CYRILLIC LETTER PALOCHKA}"), # ‘Ӏ’ + MacroTextSpec("CYRKHK", "\N{CYRILLIC CAPITAL LETTER KA WITH HOOK}"), # ‘Ӄ’ + MacroTextSpec("cyrkhk", "\N{CYRILLIC SMALL LETTER KA WITH HOOK}"), # ‘ӄ’ + MacroTextSpec("CYRLDSC", "\N{CYRILLIC CAPITAL LETTER EL WITH TAIL}"), # ‘Ӆ’ + MacroTextSpec("cyrldsc", "\N{CYRILLIC SMALL LETTER EL WITH TAIL}"), # ‘ӆ’ + MacroTextSpec("CYRNHK", "\N{CYRILLIC CAPITAL LETTER EN WITH HOOK}"), # ‘Ӈ’ + MacroTextSpec("cyrnhk", "\N{CYRILLIC SMALL LETTER EN WITH HOOK}"), # ‘ӈ’ + MacroTextSpec("CYRCHLDSC", "\N{CYRILLIC CAPITAL LETTER KHAKASSIAN CHE}"), # ‘Ӌ’ + MacroTextSpec("cyrchldsc", "\N{CYRILLIC SMALL LETTER KHAKASSIAN CHE}"), # ‘ӌ’ + MacroTextSpec("CYRMDSC", "\N{CYRILLIC CAPITAL LETTER EM WITH TAIL}"), # ‘Ӎ’ + MacroTextSpec("cyrmdsc", "\N{CYRILLIC SMALL LETTER EM WITH TAIL}"), # ‘ӎ’ + MacroTextSpec("CYRAE", "\N{CYRILLIC CAPITAL LIGATURE A IE}"), # ‘Ӕ’ + MacroTextSpec("cyrae", "\N{CYRILLIC SMALL LIGATURE A IE}"), # ‘ӕ’ + MacroTextSpec("CYRSCHWA", "\N{CYRILLIC CAPITAL LETTER SCHWA}"), # ‘Ә’ + MacroTextSpec("cyrschwa", "\N{CYRILLIC SMALL LETTER SCHWA}"), # ‘ә’ + MacroTextSpec("CYRABHDZE", "\N{CYRILLIC CAPITAL LETTER ABKHASIAN DZE}"), # ‘Ӡ’ + MacroTextSpec("cyrabhdze", "\N{CYRILLIC SMALL LETTER ABKHASIAN DZE}"), # ‘ӡ’ + MacroTextSpec("CYROTLD", "\N{CYRILLIC CAPITAL LETTER BARRED O}"), # ‘Ө’ + MacroTextSpec("cyrotld", "\N{CYRILLIC SMALL LETTER BARRED O}"), # ‘ө’ + MacroTextSpec("CYRGDSC", "\N{CYRILLIC CAPITAL LETTER GHE WITH DESCENDER}"), # ‘Ӷ’ + MacroTextSpec("cyrgdsc", "\N{CYRILLIC SMALL LETTER GHE WITH DESCENDER}"), # ‘ӷ’ + MacroTextSpec( + "CYRGDSCHCRS", "\N{CYRILLIC CAPITAL LETTER GHE WITH STROKE AND HOOK}" + ), # ‘Ӻ’ + MacroTextSpec( + "cyrgdschcrs", "\N{CYRILLIC SMALL LETTER GHE WITH STROKE AND HOOK}" + ), # ‘ӻ’ + MacroTextSpec("CYRHHK", "\N{CYRILLIC CAPITAL LETTER HA WITH HOOK}"), # ‘Ӽ’ + MacroTextSpec("cyrhhk", "\N{CYRILLIC SMALL LETTER HA WITH HOOK}"), # ‘ӽ’ + MacroTextSpec("CYRHHCRS", "\N{CYRILLIC CAPITAL LETTER HA WITH STROKE}"), # ‘Ӿ’ + MacroTextSpec("cyrhhcrs", "\N{CYRILLIC SMALL LETTER HA WITH STROKE}"), # ‘ӿ’ + MacroTextSpec("textbaht", "\N{THAI CURRENCY SYMBOL BAHT}"), # ‘฿’ + MacroTextSpec("enskip", "\N{EN QUAD}"), # ‘ ’ + MacroTextSpec("enskip", "\N{EN SPACE}"), # ‘ ’ + MacroTextSpec("textcompwordmark", "\N{ZERO WIDTH NON-JOINER}"), # ‘‌’ + MacroTextSpec("quotesinglbase", "\N{SINGLE LOW-9 QUOTATION MARK}"), # ‘‚’ + MacroTextSpec("quotedblbase", "\N{DOUBLE LOW-9 QUOTATION MARK}"), # ‘„’ + MacroTextSpec("textdagger", "\N{DAGGER}"), # ‘†’ + MacroTextSpec("textdaggerdbl", "\N{DOUBLE DAGGER}"), # ‘‡’ + MacroTextSpec("textbullet", "\N{BULLET}"), # ‘•’ + MacroTextSpec("textellipsis", "\N{HORIZONTAL ELLIPSIS}"), # ‘…’ + MacroTextSpec("textperthousand", "\N{PER MILLE SIGN}"), # ‘‰’ + MacroTextSpec("textpertenthousand", "\N{PER TEN THOUSAND SIGN}"), # ‘‱’ + MacroTextSpec("backprime", "\N{REVERSED PRIME}"), # ‘‵’ + MacroTextSpec( + "guilsinglleft", "\N{SINGLE LEFT-POINTING ANGLE QUOTATION MARK}" + ), # ‘‹’ + MacroTextSpec( + "guilsinglright", "\N{SINGLE RIGHT-POINTING ANGLE QUOTATION MARK}" + ), # ‘›’ + MacroTextSpec("textreferencemark", "\N{REFERENCE MARK}"), # ‘※’ + MacroTextSpec("textinterrobang", "\N{INTERROBANG}"), # ‘‽’ + MacroTextSpec("textfractionsolidus", "\N{FRACTION SLASH}"), # ‘⁄’ + MacroTextSpec("textasteriskcentered", "\N{LOW ASTERISK}"), # ‘⁎’ + MacroTextSpec("textdiscount", "\N{COMMERCIAL MINUS SIGN}"), # ‘⁒’ + MacroTextSpec("nolinebreak", "\N{WORD JOINER}"), # ‘⁠’ + MacroTextSpec("textcolonmonetary", "\N{COLON SIGN}"), # ‘₡’ + MacroTextSpec("textlira", "\N{LIRA SIGN}"), # ‘₤’ + MacroTextSpec("textnaira", "\N{NAIRA SIGN}"), # ‘₦’ + MacroTextSpec("textwon", "\N{WON SIGN}"), # ‘₩’ + MacroTextSpec("textdong", "\N{DONG SIGN}"), # ‘₫’ + MacroTextSpec("textpeso", "\N{PESO SIGN}"), # ‘₱’ + MacroTextSpec("textcelsius", "\N{DEGREE CELSIUS}"), # ‘℃’ + MacroTextSpec("textnumero", "\N{NUMERO SIGN}"), # ‘№’ + MacroTextSpec("textcircledP", "\N{SOUND RECORDING COPYRIGHT}"), # ‘℗’ + MacroTextSpec("wp", "\N{SCRIPT CAPITAL P}"), # ‘℘’ + MacroTextSpec("textrecipe", "\N{PRESCRIPTION TAKE}"), # ‘℞’ + MacroTextSpec("textservicemark", "\N{SERVICE MARK}"), # ‘℠’ + MacroTextSpec("texttrademark", "\N{TRADE MARK SIGN}"), # ‘™’ + MacroTextSpec("textohm", "\N{OHM SIGN}"), # ‘Ω’ + MacroTextSpec("textmho", "\N{INVERTED OHM SIGN}"), # ‘℧’ + MacroTextSpec("textestimated", "\N{ESTIMATED SYMBOL}"), # ‘℮’ + MacroTextSpec("beth", "\N{BET SYMBOL}"), # ‘ℶ’ + MacroTextSpec("gimel", "\N{GIMEL SYMBOL}"), # ‘ℷ’ + MacroTextSpec("daleth", "\N{DALET SYMBOL}"), # ‘ℸ’ + MacroTextSpec("textleftarrow", "\N{LEFTWARDS ARROW}"), # ‘←’ + MacroTextSpec("textuparrow", "\N{UPWARDS ARROW}"), # ‘↑’ + MacroTextSpec("textrightarrow", "\N{RIGHTWARDS ARROW}"), # ‘→’ + MacroTextSpec("textdownarrow", "\N{DOWNWARDS ARROW}"), # ‘↓’ + MacroTextSpec("leftrightarrow", "\N{LEFT RIGHT ARROW}"), # ‘↔’ + MacroTextSpec("updownarrow", "\N{UP DOWN ARROW}"), # ‘↕’ + MacroTextSpec("nwarrow", "\N{NORTH WEST ARROW}"), # ‘↖’ + MacroTextSpec("nearrow", "\N{NORTH EAST ARROW}"), # ‘↗’ + MacroTextSpec("searrow", "\N{SOUTH EAST ARROW}"), # ‘↘’ + MacroTextSpec("swarrow", "\N{SOUTH WEST ARROW}"), # ‘↙’ + MacroTextSpec("nleftarrow", "\N{LEFTWARDS ARROW WITH STROKE}"), # ‘↚’ + MacroTextSpec("nrightarrow", "\N{RIGHTWARDS ARROW WITH STROKE}"), # ‘↛’ + MacroTextSpec("arrowwaveleft", "\N{LEFTWARDS WAVE ARROW}"), # ‘↜’ + MacroTextSpec("arrowwaveright", "\N{RIGHTWARDS WAVE ARROW}"), # ‘↝’ + MacroTextSpec("twoheadleftarrow", "\N{LEFTWARDS TWO HEADED ARROW}"), # ‘↞’ + MacroTextSpec("twoheadrightarrow", "\N{RIGHTWARDS TWO HEADED ARROW}"), # ‘↠’ + MacroTextSpec("leftarrowtail", "\N{LEFTWARDS ARROW WITH TAIL}"), # ‘↢’ + MacroTextSpec("rightarrowtail", "\N{RIGHTWARDS ARROW WITH TAIL}"), # ‘↣’ + MacroTextSpec("mapsto", "\N{RIGHTWARDS ARROW FROM BAR}"), # ‘↦’ + MacroTextSpec("hookleftarrow", "\N{LEFTWARDS ARROW WITH HOOK}"), # ‘↩’ + MacroTextSpec("hookrightarrow", "\N{RIGHTWARDS ARROW WITH HOOK}"), # ‘↪’ + MacroTextSpec("looparrowleft", "\N{LEFTWARDS ARROW WITH LOOP}"), # ‘↫’ + MacroTextSpec("looparrowright", "\N{RIGHTWARDS ARROW WITH LOOP}"), # ‘↬’ + MacroTextSpec("leftrightsquigarrow", "\N{LEFT RIGHT WAVE ARROW}"), # ‘↭’ + MacroTextSpec("nleftrightarrow", "\N{LEFT RIGHT ARROW WITH STROKE}"), # ‘↮’ + MacroTextSpec("Lsh", "\N{UPWARDS ARROW WITH TIP LEFTWARDS}"), # ‘↰’ + MacroTextSpec("Rsh", "\N{UPWARDS ARROW WITH TIP RIGHTWARDS}"), # ‘↱’ + MacroTextSpec("curvearrowleft", "\N{ANTICLOCKWISE TOP SEMICIRCLE ARROW}"), # ‘↶’ + MacroTextSpec("curvearrowright", "\N{CLOCKWISE TOP SEMICIRCLE ARROW}"), # ‘↷’ + MacroTextSpec("circlearrowleft", "\N{ANTICLOCKWISE OPEN CIRCLE ARROW}"), # ‘↺’ + MacroTextSpec("circlearrowright", "\N{CLOCKWISE OPEN CIRCLE ARROW}"), # ‘↻’ + MacroTextSpec("leftharpoonup", "\N{LEFTWARDS HARPOON WITH BARB UPWARDS}"), # ‘↼’ + MacroTextSpec( + "leftharpoondown", "\N{LEFTWARDS HARPOON WITH BARB DOWNWARDS}" + ), # ‘↽’ + MacroTextSpec("upharpoonright", "\N{UPWARDS HARPOON WITH BARB RIGHTWARDS}"), # ‘↾’ + MacroTextSpec("upharpoonleft", "\N{UPWARDS HARPOON WITH BARB LEFTWARDS}"), # ‘↿’ + MacroTextSpec("rightharpoonup", "\N{RIGHTWARDS HARPOON WITH BARB UPWARDS}"), # ‘⇀’ + MacroTextSpec( + "rightharpoondown", "\N{RIGHTWARDS HARPOON WITH BARB DOWNWARDS}" + ), # ‘⇁’ + MacroTextSpec( + "downharpoonright", "\N{DOWNWARDS HARPOON WITH BARB RIGHTWARDS}" + ), # ‘⇂’ + MacroTextSpec( + "downharpoonleft", "\N{DOWNWARDS HARPOON WITH BARB LEFTWARDS}" + ), # ‘⇃’ + MacroTextSpec( + "rightleftarrows", "\N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW}" + ), # ‘⇄’ + MacroTextSpec( + "dblarrowupdown", "\N{UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW}" + ), # ‘⇅’ + MacroTextSpec( + "leftrightarrows", "\N{LEFTWARDS ARROW OVER RIGHTWARDS ARROW}" + ), # ‘⇆’ + MacroTextSpec("leftleftarrows", "\N{LEFTWARDS PAIRED ARROWS}"), # ‘⇇’ + MacroTextSpec("upuparrows", "\N{UPWARDS PAIRED ARROWS}"), # ‘⇈’ + MacroTextSpec("rightrightarrows", "\N{RIGHTWARDS PAIRED ARROWS}"), # ‘⇉’ + MacroTextSpec("downdownarrows", "\N{DOWNWARDS PAIRED ARROWS}"), # ‘⇊’ + MacroTextSpec( + "leftrightharpoons", "\N{LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON}" + ), # ‘⇋’ + MacroTextSpec( + "rightleftharpoons", "\N{RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON}" + ), # ‘⇌’ + MacroTextSpec("nLeftarrow", "\N{LEFTWARDS DOUBLE ARROW WITH STROKE}"), # ‘⇍’ + MacroTextSpec("nLeftrightarrow", "\N{LEFT RIGHT DOUBLE ARROW WITH STROKE}"), # ‘⇎’ + MacroTextSpec("nRightarrow", "\N{RIGHTWARDS DOUBLE ARROW WITH STROKE}"), # ‘⇏’ + MacroTextSpec("Leftarrow", "\N{LEFTWARDS DOUBLE ARROW}"), # ‘⇐’ + MacroTextSpec("Uparrow", "\N{UPWARDS DOUBLE ARROW}"), # ‘⇑’ + MacroTextSpec("Rightarrow", "\N{RIGHTWARDS DOUBLE ARROW}"), # ‘⇒’ + MacroTextSpec("Downarrow", "\N{DOWNWARDS DOUBLE ARROW}"), # ‘⇓’ + MacroTextSpec("Leftrightarrow", "\N{LEFT RIGHT DOUBLE ARROW}"), # ‘⇔’ + MacroTextSpec("Updownarrow", "\N{UP DOWN DOUBLE ARROW}"), # ‘⇕’ + MacroTextSpec("Lleftarrow", "\N{LEFTWARDS TRIPLE ARROW}"), # ‘⇚’ + MacroTextSpec("Rrightarrow", "\N{RIGHTWARDS TRIPLE ARROW}"), # ‘⇛’ + MacroTextSpec("rightsquigarrow", "\N{RIGHTWARDS SQUIGGLE ARROW}"), # ‘⇝’ + MacroTextSpec( + "DownArrowUpArrow", "\N{DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW}" + ), # ‘⇵’ + MacroTextSpec("blacksquare", "\N{END OF PROOF}"), # ‘∎’ + MacroTextSpec("dotplus", "\N{DOT PLUS}"), # ‘∔’ + MacroTextSpec("rightangle", "\N{RIGHT ANGLE}"), # ‘∟’ + MacroTextSpec("angle", "\N{ANGLE}"), # ‘∠’ + MacroTextSpec("measuredangle", "\N{MEASURED ANGLE}"), # ‘∡’ + MacroTextSpec("sphericalangle", "\N{SPHERICAL ANGLE}"), # ‘∢’ + MacroTextSpec("surfintegral", "\N{SURFACE INTEGRAL}"), # ‘∯’ + MacroTextSpec("volintegral", "\N{VOLUME INTEGRAL}"), # ‘∰’ + MacroTextSpec("clwintegral", "\N{CLOCKWISE INTEGRAL}"), # ‘∱’ + MacroTextSpec("therefore", "\N{THEREFORE}"), # ‘∴’ + MacroTextSpec("because", "\N{BECAUSE}"), # ‘∵’ + MacroTextSpec("homothetic", "\N{HOMOTHETIC}"), # ‘∻’ + MacroTextSpec("lazysinv", "\N{INVERTED LAZY S}"), # ‘∾’ + MacroTextSpec("wr", "\N{WREATH PRODUCT}"), # ‘≀’ + MacroTextSpec("cong", "\N{APPROXIMATELY EQUAL TO}"), # ‘≅’ + MacroTextSpec( + "approxnotequal", "\N{APPROXIMATELY BUT NOT ACTUALLY EQUAL TO}" + ), # ‘≆’ + MacroTextSpec("approxeq", "\N{ALMOST EQUAL OR EQUAL TO}"), # ‘≊’ + MacroTextSpec("tildetrpl", "\N{TRIPLE TILDE}"), # ‘≋’ + MacroTextSpec("allequal", "\N{ALL EQUAL TO}"), # ‘≌’ + MacroTextSpec("asymp", "\N{EQUIVALENT TO}"), # ‘≍’ + MacroTextSpec("Bumpeq", "\N{GEOMETRICALLY EQUIVALENT TO}"), # ‘≎’ + MacroTextSpec("bumpeq", "\N{DIFFERENCE BETWEEN}"), # ‘≏’ + MacroTextSpec("doteq", "\N{APPROACHES THE LIMIT}"), # ‘≐’ + MacroTextSpec("doteqdot", "\N{GEOMETRICALLY EQUAL TO}"), # ‘≑’ + MacroTextSpec("fallingdotseq", "\N{APPROXIMATELY EQUAL TO OR THE IMAGE OF}"), # ‘≒’ + MacroTextSpec("risingdotseq", "\N{IMAGE OF OR APPROXIMATELY EQUAL TO}"), # ‘≓’ + MacroTextSpec("eqcirc", "\N{RING IN EQUAL TO}"), # ‘≖’ + MacroTextSpec("circeq", "\N{RING EQUAL TO}"), # ‘≗’ + MacroTextSpec("estimates", "\N{ESTIMATES}"), # ‘≙’ + MacroTextSpec("starequal", "\N{STAR EQUALS}"), # ‘≛’ + MacroTextSpec("triangleq", "\N{DELTA EQUAL TO}"), # ‘≜’ + MacroTextSpec("between", "\N{BETWEEN}"), # ‘≬’ + MacroTextSpec("notlessgreater", "\N{NEITHER LESS-THAN NOR GREATER-THAN}"), # ‘≸’ + MacroTextSpec("notgreaterless", "\N{NEITHER GREATER-THAN NOR LESS-THAN}"), # ‘≹’ + MacroTextSpec("uplus", "\N{MULTISET UNION}"), # ‘⊎’ + MacroTextSpec("sqsubset", "\N{SQUARE IMAGE OF}"), # ‘⊏’ + MacroTextSpec("sqsupset", "\N{SQUARE ORIGINAL OF}"), # ‘⊐’ + MacroTextSpec("sqsubseteq", "\N{SQUARE IMAGE OF OR EQUAL TO}"), # ‘⊑’ + MacroTextSpec("sqsupseteq", "\N{SQUARE ORIGINAL OF OR EQUAL TO}"), # ‘⊒’ + MacroTextSpec("sqcap", "\N{SQUARE CAP}"), # ‘⊓’ + MacroTextSpec("sqcup", "\N{SQUARE CUP}"), # ‘⊔’ + MacroTextSpec("ominus", "\N{CIRCLED MINUS}"), # ‘⊖’ + MacroTextSpec("oslash", "\N{CIRCLED DIVISION SLASH}"), # ‘⊘’ + MacroTextSpec("odot", "\N{CIRCLED DOT OPERATOR}"), # ‘⊙’ + MacroTextSpec("circledcirc", "\N{CIRCLED RING OPERATOR}"), # ‘⊚’ + MacroTextSpec("circledast", "\N{CIRCLED ASTERISK OPERATOR}"), # ‘⊛’ + MacroTextSpec("circleddash", "\N{CIRCLED DASH}"), # ‘⊝’ + MacroTextSpec("boxplus", "\N{SQUARED PLUS}"), # ‘⊞’ + MacroTextSpec("boxminus", "\N{SQUARED MINUS}"), # ‘⊟’ + MacroTextSpec("boxtimes", "\N{SQUARED TIMES}"), # ‘⊠’ + MacroTextSpec("boxdot", "\N{SQUARED DOT OPERATOR}"), # ‘⊡’ + MacroTextSpec("vdash", "\N{RIGHT TACK}"), # ‘⊢’ + MacroTextSpec("dashv", "\N{LEFT TACK}"), # ‘⊣’ + MacroTextSpec("top", "\N{DOWN TACK}"), # ‘⊤’ + MacroTextSpec("perp", "\N{UP TACK}"), # ‘⊥’ + MacroTextSpec("truestate", "\N{MODELS}"), # ‘⊧’ + MacroTextSpec("forcesextra", "\N{TRUE}"), # ‘⊨’ + MacroTextSpec("Vdash", "\N{FORCES}"), # ‘⊩’ + MacroTextSpec("Vvdash", "\N{TRIPLE VERTICAL BAR RIGHT TURNSTILE}"), # ‘⊪’ + MacroTextSpec("VDash", "\N{DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE}"), # ‘⊫’ + MacroTextSpec("nvdash", "\N{DOES NOT PROVE}"), # ‘⊬’ + MacroTextSpec("nvDash", "\N{NOT TRUE}"), # ‘⊭’ + MacroTextSpec("nVdash", "\N{DOES NOT FORCE}"), # ‘⊮’ + MacroTextSpec( + "nVDash", "\N{NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE}" + ), # ‘⊯’ + MacroTextSpec("vartriangleleft", "\N{NORMAL SUBGROUP OF}"), # ‘⊲’ + MacroTextSpec("vartriangleright", "\N{CONTAINS AS NORMAL SUBGROUP}"), # ‘⊳’ + MacroTextSpec("trianglelefteq", "\N{NORMAL SUBGROUP OF OR EQUAL TO}"), # ‘⊴’ + MacroTextSpec( + "trianglerighteq", "\N{CONTAINS AS NORMAL SUBGROUP OR EQUAL TO}" + ), # ‘⊵’ + MacroTextSpec("original", "\N{ORIGINAL OF}"), # ‘⊶’ + MacroTextSpec("image", "\N{IMAGE OF}"), # ‘⊷’ + MacroTextSpec("multimap", "\N{MULTIMAP}"), # ‘⊸’ + MacroTextSpec("hermitconjmatrix", "\N{HERMITIAN CONJUGATE MATRIX}"), # ‘⊹’ + MacroTextSpec("intercal", "\N{INTERCALATE}"), # ‘⊺’ + MacroTextSpec("veebar", "\N{XOR}"), # ‘⊻’ + MacroTextSpec("rightanglearc", "\N{RIGHT ANGLE WITH ARC}"), # ‘⊾’ + MacroTextSpec("bigwedge", "\N{N-ARY LOGICAL AND}"), # ‘⋀’ + MacroTextSpec("bigvee", "\N{N-ARY LOGICAL OR}"), # ‘⋁’ + MacroTextSpec("bigcap", "\N{N-ARY INTERSECTION}"), # ‘⋂’ + MacroTextSpec("bigcup", "\N{N-ARY UNION}"), # ‘⋃’ + MacroTextSpec("diamond", "\N{DIAMOND OPERATOR}"), # ‘⋄’ + MacroTextSpec("star", "\N{STAR OPERATOR}"), # ‘⋆’ + MacroTextSpec("divideontimes", "\N{DIVISION TIMES}"), # ‘⋇’ + MacroTextSpec("bowtie", "\N{BOWTIE}"), # ‘⋈’ + MacroTextSpec("ltimes", "\N{LEFT NORMAL FACTOR SEMIDIRECT PRODUCT}"), # ‘⋉’ + MacroTextSpec("rtimes", "\N{RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT}"), # ‘⋊’ + MacroTextSpec("leftthreetimes", "\N{LEFT SEMIDIRECT PRODUCT}"), # ‘⋋’ + MacroTextSpec("rightthreetimes", "\N{RIGHT SEMIDIRECT PRODUCT}"), # ‘⋌’ + MacroTextSpec("backsimeq", "\N{REVERSED TILDE EQUALS}"), # ‘⋍’ + MacroTextSpec("curlyvee", "\N{CURLY LOGICAL OR}"), # ‘⋎’ + MacroTextSpec("curlywedge", "\N{CURLY LOGICAL AND}"), # ‘⋏’ + MacroTextSpec("Subset", "\N{DOUBLE SUBSET}"), # ‘⋐’ + MacroTextSpec("Supset", "\N{DOUBLE SUPERSET}"), # ‘⋑’ + MacroTextSpec("Cap", "\N{DOUBLE INTERSECTION}"), # ‘⋒’ + MacroTextSpec("Cup", "\N{DOUBLE UNION}"), # ‘⋓’ + MacroTextSpec("pitchfork", "\N{PITCHFORK}"), # ‘⋔’ + MacroTextSpec("lessdot", "\N{LESS-THAN WITH DOT}"), # ‘⋖’ + MacroTextSpec("gtrdot", "\N{GREATER-THAN WITH DOT}"), # ‘⋗’ + MacroTextSpec("verymuchless", "\N{VERY MUCH LESS-THAN}"), # ‘⋘’ + MacroTextSpec("verymuchgreater", "\N{VERY MUCH GREATER-THAN}"), # ‘⋙’ + MacroTextSpec("lesseqgtr", "\N{LESS-THAN EQUAL TO OR GREATER-THAN}"), # ‘⋚’ + MacroTextSpec("gtreqless", "\N{GREATER-THAN EQUAL TO OR LESS-THAN}"), # ‘⋛’ + MacroTextSpec("curlyeqprec", "\N{EQUAL TO OR PRECEDES}"), # ‘⋞’ + MacroTextSpec("curlyeqsucc", "\N{EQUAL TO OR SUCCEEDS}"), # ‘⋟’ + MacroTextSpec("lnsim", "\N{LESS-THAN BUT NOT EQUIVALENT TO}"), # ‘⋦’ + MacroTextSpec("gnsim", "\N{GREATER-THAN BUT NOT EQUIVALENT TO}"), # ‘⋧’ + MacroTextSpec("precedesnotsimilar", "\N{PRECEDES BUT NOT EQUIVALENT TO}"), # ‘⋨’ + MacroTextSpec("succnsim", "\N{SUCCEEDS BUT NOT EQUIVALENT TO}"), # ‘⋩’ + MacroTextSpec("ntriangleleft", "\N{NOT NORMAL SUBGROUP OF}"), # ‘⋪’ + MacroTextSpec("ntriangleright", "\N{DOES NOT CONTAIN AS NORMAL SUBGROUP}"), # ‘⋫’ + MacroTextSpec("ntrianglelefteq", "\N{NOT NORMAL SUBGROUP OF OR EQUAL TO}"), # ‘⋬’ + MacroTextSpec( + "ntrianglerighteq", "\N{DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL}" + ), # ‘⋭’ + MacroTextSpec("vdots", "\N{VERTICAL ELLIPSIS}"), # ‘⋮’ + MacroTextSpec("udots", "\N{UP RIGHT DIAGONAL ELLIPSIS}"), # ‘⋰’ + MacroTextSpec("barwedge", "\N{PROJECTIVE}"), # ‘⌅’ + MacroTextSpec("varperspcorrespond", "\N{PERSPECTIVE}"), # ‘⌆’ + MacroTextSpec("lceil", "\N{LEFT CEILING}"), # ‘⌈’ + MacroTextSpec("rceil", "\N{RIGHT CEILING}"), # ‘⌉’ + MacroTextSpec("lfloor", "\N{LEFT FLOOR}"), # ‘⌊’ + MacroTextSpec("rfloor", "\N{RIGHT FLOOR}"), # ‘⌋’ + MacroTextSpec("recorder", "\N{TELEPHONE RECORDER}"), # ‘⌕’ + MacroTextSpec("ulcorner", "\N{TOP LEFT CORNER}"), # ‘⌜’ + MacroTextSpec("urcorner", "\N{TOP RIGHT CORNER}"), # ‘⌝’ + MacroTextSpec("llcorner", "\N{BOTTOM LEFT CORNER}"), # ‘⌞’ + MacroTextSpec("lrcorner", "\N{BOTTOM RIGHT CORNER}"), # ‘⌟’ + MacroTextSpec("frown", "\N{FROWN}"), # ‘⌢’ + MacroTextSpec("smile", "\N{SMILE}"), # ‘⌣’ + MacroTextSpec( + "lmoustache", "\N{UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION}" + ), # ‘⎰’ + MacroTextSpec( + "rmoustache", "\N{UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION}" + ), # ‘⎱’ + MacroTextSpec("textlangle", "\N{LEFT-POINTING ANGLE BRACKET}"), # ‘〈’ + MacroTextSpec("textrangle", "\N{RIGHT-POINTING ANGLE BRACKET}"), # ‘〉’ + MacroTextSpec("textblank", "\N{BLANK SYMBOL}"), # ‘␢’ + MacroTextSpec("textvisiblespace", "\N{OPEN BOX}"), # ‘␣’ + MacroTextSpec("blacksquare", "\N{BLACK SQUARE}"), # ‘■’ + MacroTextSpec("square", "\N{WHITE SQUARE}"), # ‘□’ + MacroTextSpec("bigtriangleup", "\N{WHITE UP-POINTING TRIANGLE}"), # ‘△’ + MacroTextSpec("blacktriangle", "\N{BLACK UP-POINTING SMALL TRIANGLE}"), # ‘▴’ + MacroTextSpec("vartriangle", "\N{WHITE UP-POINTING SMALL TRIANGLE}"), # ‘▵’ + MacroTextSpec( + "blacktriangleright", "\N{BLACK RIGHT-POINTING SMALL TRIANGLE}" + ), # ‘▸’ + MacroTextSpec("triangleright", "\N{WHITE RIGHT-POINTING SMALL TRIANGLE}"), # ‘▹’ + MacroTextSpec("bigtriangledown", "\N{WHITE DOWN-POINTING TRIANGLE}"), # ‘▽’ + MacroTextSpec("blacktriangledown", "\N{BLACK DOWN-POINTING SMALL TRIANGLE}"), # ‘▾’ + MacroTextSpec("triangledown", "\N{WHITE DOWN-POINTING SMALL TRIANGLE}"), # ‘▿’ + MacroTextSpec("blacktriangleleft", "\N{BLACK LEFT-POINTING SMALL TRIANGLE}"), # ‘◂’ + MacroTextSpec("triangleleft", "\N{WHITE LEFT-POINTING SMALL TRIANGLE}"), # ‘◃’ + MacroTextSpec("lozenge", "\N{LOZENGE}"), # ‘◊’ + MacroTextSpec("bigcirc", "\N{WHITE CIRCLE}"), # ‘○’ + MacroTextSpec("textopenbullet", "\N{WHITE BULLET}"), # ‘◦’ + MacroTextSpec("textbigcircle", "\N{LARGE CIRCLE}"), # ‘◯’ + MacroTextSpec("diamond", "\N{WHITE DIAMOND SUIT}"), # ‘♢’ + MacroTextSpec("textmusicalnote", "\N{EIGHTH NOTE}"), # ‘♪’ + MacroTextSpec("quarternote", "\N{QUARTER NOTE}"), # ‘♩’ + MacroTextSpec("flat", "\N{MUSIC FLAT SIGN}"), # ‘♭’ + MacroTextSpec("natural", "\N{MUSIC NATURAL SIGN}"), # ‘♮’ + MacroTextSpec("sharp", "\N{MUSIC SHARP SIGN}"), # ‘♯’ + MacroTextSpec("longleftrightarrow", "\N{LONG LEFT RIGHT ARROW}"), # ‘⟷’ + MacroTextSpec("Longleftarrow", "\N{LONG LEFTWARDS DOUBLE ARROW}"), # ‘⟸’ + MacroTextSpec("Longrightarrow", "\N{LONG RIGHTWARDS DOUBLE ARROW}"), # ‘⟹’ + MacroTextSpec("Longleftrightarrow", "\N{LONG LEFT RIGHT DOUBLE ARROW}"), # ‘⟺’ + MacroTextSpec("longmapsto", "\N{LONG RIGHTWARDS ARROW FROM BAR}"), # ‘⟼’ + MacroTextSpec("blacklozenge", "\N{BLACK LOZENGE}"), # ‘⧫’ + MacroTextSpec("clockoint", "\N{INTEGRAL AVERAGE WITH SLASH}"), # ‘⨏’ + MacroTextSpec("sqrint", "\N{QUATERNION INTEGRAL OPERATOR}"), # ‘⨖’ + MacroTextSpec("amalg", "\N{AMALGAMATION OR COPRODUCT}"), # ‘⨿’ + MacroTextSpec("lessapprox", "\N{LESS-THAN OR APPROXIMATE}"), # ‘⪅’ + MacroTextSpec("gtrapprox", "\N{GREATER-THAN OR APPROXIMATE}"), # ‘⪆’ + MacroTextSpec("lneq", "\N{LESS-THAN AND SINGLE-LINE NOT EQUAL TO}"), # ‘⪇’ + MacroTextSpec("gneq", "\N{GREATER-THAN AND SINGLE-LINE NOT EQUAL TO}"), # ‘⪈’ + MacroTextSpec("lnapprox", "\N{LESS-THAN AND NOT APPROXIMATE}"), # ‘⪉’ + MacroTextSpec("gnapprox", "\N{GREATER-THAN AND NOT APPROXIMATE}"), # ‘⪊’ + MacroTextSpec( + "lesseqqgtr", "\N{LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN}" + ), # ‘⪋’ + MacroTextSpec( + "gtreqqless", "\N{GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN}" + ), # ‘⪌’ + MacroTextSpec("eqslantless", "\N{SLANTED EQUAL TO OR LESS-THAN}"), # ‘⪕’ + MacroTextSpec("eqslantgtr", "\N{SLANTED EQUAL TO OR GREATER-THAN}"), # ‘⪖’ + MacroTextSpec("precneqq", "\N{PRECEDES ABOVE NOT EQUAL TO}"), # ‘⪵’ + MacroTextSpec("succneqq", "\N{SUCCEEDS ABOVE NOT EQUAL TO}"), # ‘⪶’ + MacroTextSpec("precapprox", "\N{PRECEDES ABOVE ALMOST EQUAL TO}"), # ‘⪷’ + MacroTextSpec("succapprox", "\N{SUCCEEDS ABOVE ALMOST EQUAL TO}"), # ‘⪸’ + MacroTextSpec("precnapprox", "\N{PRECEDES ABOVE NOT ALMOST EQUAL TO}"), # ‘⪹’ + MacroTextSpec("succnapprox", "\N{SUCCEEDS ABOVE NOT ALMOST EQUAL TO}"), # ‘⪺’ + MacroTextSpec("subseteqq", "\N{SUBSET OF ABOVE EQUALS SIGN}"), # ‘⫅’ + MacroTextSpec("supseteqq", "\N{SUPERSET OF ABOVE EQUALS SIGN}"), # ‘⫆’ + MacroTextSpec("subsetneqq", "\N{SUBSET OF ABOVE NOT EQUAL TO}"), # ‘⫋’ + MacroTextSpec("supsetneqq", "\N{SUPERSET OF ABOVE NOT EQUAL TO}"), # ‘⫌’ + # Rules from latexencode defaults 'unicode-xml' + MacroTextSpec("textdollar", "\N{DOLLAR SIGN}"), # ‘$’ + MacroTextSpec("textquotesingle", "\N{APOSTROPHE}"), # ‘'’ + MacroTextSpec("textasciigrave", "\N{GRAVE ACCENT}"), # ‘`’ + MacroTextSpec("lbrace", "\N{LEFT CURLY BRACKET}"), # ‘{’ + MacroTextSpec("rbrace", "\N{RIGHT CURLY BRACKET}"), # ‘}’ + MacroTextSpec("textasciitilde", "\N{TILDE}"), # ‘~’ + MacroTextSpec("textexclamdown", "\N{INVERTED EXCLAMATION MARK}"), # ‘¡’ + MacroTextSpec("textcent", "\N{CENT SIGN}"), # ‘¢’ + MacroTextSpec("textsterling", "\N{POUND SIGN}"), # ‘£’ + MacroTextSpec("textcurrency", "\N{CURRENCY SIGN}"), # ‘¤’ + MacroTextSpec("textyen", "\N{YEN SIGN}"), # ‘¥’ + MacroTextSpec("textbrokenbar", "\N{BROKEN BAR}"), # ‘¦’ + MacroTextSpec("textsection", "\N{SECTION SIGN}"), # ‘§’ + MacroTextSpec("textasciidieresis", "\N{DIAERESIS}"), # ‘¨’ + MacroTextSpec("textcopyright", "\N{COPYRIGHT SIGN}"), # ‘©’ + MacroTextSpec("textordfeminine", "\N{FEMININE ORDINAL INDICATOR}"), # ‘ª’ + MacroTextSpec( + "guillemotleft", "\N{LEFT-POINTING DOUBLE ANGLE QUOTATION MARK}" + ), # ‘«’ + MacroTextSpec("lnot", "\N{NOT SIGN}"), # ‘¬’ + MacroTextSpec("-", "\N{SOFT HYPHEN}"), # ‘­’ + MacroTextSpec("textregistered", "\N{REGISTERED SIGN}"), # ‘®’ + MacroTextSpec("textasciimacron", "\N{MACRON}"), # ‘¯’ + MacroTextSpec("textdegree", "\N{DEGREE SIGN}"), # ‘°’ + MacroTextSpec("textasciiacute", "\N{ACUTE ACCENT}"), # ‘´’ + MacroTextSpec("textparagraph", "\N{PILCROW SIGN}"), # ‘¶’ + MacroTextSpec("textordmasculine", "\N{MASCULINE ORDINAL INDICATOR}"), # ‘º’ + MacroTextSpec( + "guillemotright", "\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}" + ), # ‘»’ + MacroTextSpec("textonequarter", "\N{VULGAR FRACTION ONE QUARTER}"), # ‘¼’ + MacroTextSpec("textonehalf", "\N{VULGAR FRACTION ONE HALF}"), # ‘½’ + MacroTextSpec("textthreequarters", "\N{VULGAR FRACTION THREE QUARTERS}"), # ‘¾’ + MacroTextSpec("textquestiondown", "\N{INVERTED QUESTION MARK}"), # ‘¿’ + MacroTextSpec("DH", "\N{LATIN CAPITAL LETTER ETH}"), # ‘Ð’ + MacroTextSpec("texttimes", "\N{MULTIPLICATION SIGN}"), # ‘×’ + MacroTextSpec("TH", "\N{LATIN CAPITAL LETTER THORN}"), # ‘Þ’ + MacroTextSpec("dh", "\N{LATIN SMALL LETTER ETH}"), # ‘ð’ + MacroTextSpec("div", "\N{DIVISION SIGN}"), # ‘÷’ + MacroTextSpec("th", "\N{LATIN SMALL LETTER THORN}"), # ‘þ’ + MacroTextSpec("DJ", "\N{LATIN CAPITAL LETTER D WITH STROKE}"), # ‘Đ’ + MacroTextSpec("dj", "\N{LATIN SMALL LETTER D WITH STROKE}"), # ‘đ’ + MacroTextSpec("NG", "\N{LATIN CAPITAL LETTER ENG}"), # ‘Ŋ’ + MacroTextSpec("ng", "\N{LATIN SMALL LETTER ENG}"), # ‘ŋ’ + MacroTextSpec("texthvlig", "\N{LATIN SMALL LETTER HV}"), # ‘ƕ’ + MacroTextSpec("textnrleg", "\N{LATIN SMALL LETTER N WITH LONG RIGHT LEG}"), # ‘ƞ’ + MacroTextSpec("eth", "\N{LATIN LETTER REVERSED ESH LOOP}"), # ‘ƪ’ + MacroTextSpec("textdoublepipe", "\N{LATIN LETTER ALVEOLAR CLICK}"), # ‘ǂ’ + MacroTextSpec("textphi", "\N{LATIN SMALL LETTER PHI}"), # ‘ɸ’ + MacroTextSpec("textturnk", "\N{LATIN SMALL LETTER TURNED K}"), # ‘ʞ’ + MacroTextSpec("textasciicaron", "\N{CARON}"), # ‘ˇ’ + MacroTextSpec("textasciibreve", "\N{BREVE}"), # ‘˘’ + MacroTextSpec("textperiodcentered", "\N{DOT ABOVE}"), # ‘˙’ + MacroTextSpec("texttildelow", "\N{SMALL TILDE}"), # ‘˜’ + MacroTextSpec("texttheta", "\N{GREEK SMALL LETTER THETA}"), # ‘θ’ + MacroTextSpec("textvartheta", "\N{GREEK THETA SYMBOL}"), # ‘ϑ’ + MacroTextSpec("Stigma", "\N{GREEK LETTER STIGMA}"), # ‘Ϛ’ + MacroTextSpec("Digamma", "\N{GREEK LETTER DIGAMMA}"), # ‘Ϝ’ + MacroTextSpec("digamma", "\N{GREEK SMALL LETTER DIGAMMA}"), # ‘ϝ’ + MacroTextSpec("Koppa", "\N{GREEK LETTER KOPPA}"), # ‘Ϟ’ + MacroTextSpec("Sampi", "\N{GREEK LETTER SAMPI}"), # ‘Ϡ’ + MacroTextSpec("varkappa", "\N{GREEK KAPPA SYMBOL}"), # ‘ϰ’ + MacroTextSpec("textTheta", "\N{GREEK CAPITAL THETA SYMBOL}"), # ‘ϴ’ + MacroTextSpec("backepsilon", "\N{GREEK REVERSED LUNATE EPSILON SYMBOL}"), # ‘϶’ + MacroTextSpec("textdagger", "\N{DAGGER}"), # ‘†’ + MacroTextSpec("textdaggerdbl", "\N{DOUBLE DAGGER}"), # ‘‡’ + MacroTextSpec("textbullet", "\N{BULLET}"), # ‘•’ + MacroTextSpec("textperthousand", "\N{PER MILLE SIGN}"), # ‘‰’ + MacroTextSpec("textpertenthousand", "\N{PER TEN THOUSAND SIGN}"), # ‘‱’ + MacroTextSpec("backprime", "\N{REVERSED PRIME}"), # ‘‵’ + MacroTextSpec( + "guilsinglleft", "\N{SINGLE LEFT-POINTING ANGLE QUOTATION MARK}" + ), # ‘‹’ + MacroTextSpec( + "guilsinglright", "\N{SINGLE RIGHT-POINTING ANGLE QUOTATION MARK}" + ), # ‘›’ + MacroTextSpec("nolinebreak", "\N{WORD JOINER}"), # ‘⁠’ + MacroTextSpec("dddot", "\N{COMBINING THREE DOTS ABOVE}"), # ‘⃛’ + MacroTextSpec("ddddot", "\N{COMBINING FOUR DOTS ABOVE}"), # ‘⃜’ + MacroTextSpec("hslash", "\N{PLANCK CONSTANT OVER TWO PI}"), # ‘ℏ’ + MacroTextSpec("wp", "\N{SCRIPT CAPITAL P}"), # ‘℘’ + MacroTextSpec("texttrademark", "\N{TRADE MARK SIGN}"), # ‘™’ + MacroTextSpec("mho", "\N{INVERTED OHM SIGN}"), # ‘℧’ + MacroTextSpec("beth", "\N{BET SYMBOL}"), # ‘ℶ’ + MacroTextSpec("gimel", "\N{GIMEL SYMBOL}"), # ‘ℷ’ + MacroTextSpec("daleth", "\N{DALET SYMBOL}"), # ‘ℸ’ + MacroTextSpec("leftrightarrow", "\N{LEFT RIGHT ARROW}"), # ‘↔’ + MacroTextSpec("updownarrow", "\N{UP DOWN ARROW}"), # ‘↕’ + MacroTextSpec("nwarrow", "\N{NORTH WEST ARROW}"), # ‘↖’ + MacroTextSpec("nearrow", "\N{NORTH EAST ARROW}"), # ‘↗’ + MacroTextSpec("searrow", "\N{SOUTH EAST ARROW}"), # ‘↘’ + MacroTextSpec("swarrow", "\N{SOUTH WEST ARROW}"), # ‘↙’ + MacroTextSpec("nleftarrow", "\N{LEFTWARDS ARROW WITH STROKE}"), # ‘↚’ + MacroTextSpec("nrightarrow", "\N{RIGHTWARDS ARROW WITH STROKE}"), # ‘↛’ + MacroTextSpec("arrowwaveleft", "\N{LEFTWARDS WAVE ARROW}"), # ‘↜’ + MacroTextSpec("arrowwaveright", "\N{RIGHTWARDS WAVE ARROW}"), # ‘↝’ + MacroTextSpec("twoheadleftarrow", "\N{LEFTWARDS TWO HEADED ARROW}"), # ‘↞’ + MacroTextSpec("twoheadrightarrow", "\N{RIGHTWARDS TWO HEADED ARROW}"), # ‘↠’ + MacroTextSpec("leftarrowtail", "\N{LEFTWARDS ARROW WITH TAIL}"), # ‘↢’ + MacroTextSpec("rightarrowtail", "\N{RIGHTWARDS ARROW WITH TAIL}"), # ‘↣’ + MacroTextSpec("mapsto", "\N{RIGHTWARDS ARROW FROM BAR}"), # ‘↦’ + MacroTextSpec("hookleftarrow", "\N{LEFTWARDS ARROW WITH HOOK}"), # ‘↩’ + MacroTextSpec("hookrightarrow", "\N{RIGHTWARDS ARROW WITH HOOK}"), # ‘↪’ + MacroTextSpec("looparrowleft", "\N{LEFTWARDS ARROW WITH LOOP}"), # ‘↫’ + MacroTextSpec("looparrowright", "\N{RIGHTWARDS ARROW WITH LOOP}"), # ‘↬’ + MacroTextSpec("leftrightsquigarrow", "\N{LEFT RIGHT WAVE ARROW}"), # ‘↭’ + MacroTextSpec("nleftrightarrow", "\N{LEFT RIGHT ARROW WITH STROKE}"), # ‘↮’ + MacroTextSpec("Lsh", "\N{UPWARDS ARROW WITH TIP LEFTWARDS}"), # ‘↰’ + MacroTextSpec("Rsh", "\N{UPWARDS ARROW WITH TIP RIGHTWARDS}"), # ‘↱’ + MacroTextSpec("curvearrowleft", "\N{ANTICLOCKWISE TOP SEMICIRCLE ARROW}"), # ‘↶’ + MacroTextSpec("curvearrowright", "\N{CLOCKWISE TOP SEMICIRCLE ARROW}"), # ‘↷’ + MacroTextSpec("circlearrowleft", "\N{ANTICLOCKWISE OPEN CIRCLE ARROW}"), # ‘↺’ + MacroTextSpec("circlearrowright", "\N{CLOCKWISE OPEN CIRCLE ARROW}"), # ‘↻’ + MacroTextSpec("leftharpoonup", "\N{LEFTWARDS HARPOON WITH BARB UPWARDS}"), # ‘↼’ + MacroTextSpec( + "leftharpoondown", "\N{LEFTWARDS HARPOON WITH BARB DOWNWARDS}" + ), # ‘↽’ + MacroTextSpec("upharpoonright", "\N{UPWARDS HARPOON WITH BARB RIGHTWARDS}"), # ‘↾’ + MacroTextSpec("upharpoonleft", "\N{UPWARDS HARPOON WITH BARB LEFTWARDS}"), # ‘↿’ + MacroTextSpec("rightharpoonup", "\N{RIGHTWARDS HARPOON WITH BARB UPWARDS}"), # ‘⇀’ + MacroTextSpec( + "rightharpoondown", "\N{RIGHTWARDS HARPOON WITH BARB DOWNWARDS}" + ), # ‘⇁’ + MacroTextSpec( + "downharpoonright", "\N{DOWNWARDS HARPOON WITH BARB RIGHTWARDS}" + ), # ‘⇂’ + MacroTextSpec( + "downharpoonleft", "\N{DOWNWARDS HARPOON WITH BARB LEFTWARDS}" + ), # ‘⇃’ + MacroTextSpec( + "rightleftarrows", "\N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW}" + ), # ‘⇄’ + MacroTextSpec( + "dblarrowupdown", "\N{UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW}" + ), # ‘⇅’ + MacroTextSpec( + "leftrightarrows", "\N{LEFTWARDS ARROW OVER RIGHTWARDS ARROW}" + ), # ‘⇆’ + MacroTextSpec("leftleftarrows", "\N{LEFTWARDS PAIRED ARROWS}"), # ‘⇇’ + MacroTextSpec("upuparrows", "\N{UPWARDS PAIRED ARROWS}"), # ‘⇈’ + MacroTextSpec("rightrightarrows", "\N{RIGHTWARDS PAIRED ARROWS}"), # ‘⇉’ + MacroTextSpec("downdownarrows", "\N{DOWNWARDS PAIRED ARROWS}"), # ‘⇊’ + MacroTextSpec( + "leftrightharpoons", "\N{LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON}" + ), # ‘⇋’ + MacroTextSpec( + "rightleftharpoons", "\N{RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON}" + ), # ‘⇌’ + MacroTextSpec("nLeftarrow", "\N{LEFTWARDS DOUBLE ARROW WITH STROKE}"), # ‘⇍’ + MacroTextSpec("nLeftrightarrow", "\N{LEFT RIGHT DOUBLE ARROW WITH STROKE}"), # ‘⇎’ + MacroTextSpec("nRightarrow", "\N{RIGHTWARDS DOUBLE ARROW WITH STROKE}"), # ‘⇏’ + MacroTextSpec("Leftarrow", "\N{LEFTWARDS DOUBLE ARROW}"), # ‘⇐’ + MacroTextSpec("Uparrow", "\N{UPWARDS DOUBLE ARROW}"), # ‘⇑’ + MacroTextSpec("Rightarrow", "\N{RIGHTWARDS DOUBLE ARROW}"), # ‘⇒’ + MacroTextSpec("Downarrow", "\N{DOWNWARDS DOUBLE ARROW}"), # ‘⇓’ + MacroTextSpec("Leftrightarrow", "\N{LEFT RIGHT DOUBLE ARROW}"), # ‘⇔’ + MacroTextSpec("Updownarrow", "\N{UP DOWN DOUBLE ARROW}"), # ‘⇕’ + MacroTextSpec("Lleftarrow", "\N{LEFTWARDS TRIPLE ARROW}"), # ‘⇚’ + MacroTextSpec("Rrightarrow", "\N{RIGHTWARDS TRIPLE ARROW}"), # ‘⇛’ + MacroTextSpec("rightsquigarrow", "\N{RIGHTWARDS SQUIGGLE ARROW}"), # ‘⇝’ + MacroTextSpec( + "DownArrowUpArrow", "\N{DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW}" + ), # ‘⇵’ + MacroTextSpec("dotplus", "\N{DOT PLUS}"), # ‘∔’ + MacroTextSpec("surd", "\N{SQUARE ROOT}"), # ‘√’ + MacroTextSpec("rightangle", "\N{RIGHT ANGLE}"), # ‘∟’ + MacroTextSpec("angle", "\N{ANGLE}"), # ‘∠’ + MacroTextSpec("measuredangle", "\N{MEASURED ANGLE}"), # ‘∡’ + MacroTextSpec("sphericalangle", "\N{SPHERICAL ANGLE}"), # ‘∢’ + MacroTextSpec("surfintegral", "\N{SURFACE INTEGRAL}"), # ‘∯’ + MacroTextSpec("volintegral", "\N{VOLUME INTEGRAL}"), # ‘∰’ + MacroTextSpec("clwintegral", "\N{CLOCKWISE INTEGRAL}"), # ‘∱’ + MacroTextSpec("therefore", "\N{THEREFORE}"), # ‘∴’ + MacroTextSpec("because", "\N{BECAUSE}"), # ‘∵’ + MacroTextSpec("Colon", "\N{PROPORTION}"), # ‘∷’ + MacroTextSpec("homothetic", "\N{HOMOTHETIC}"), # ‘∻’ + MacroTextSpec("lazysinv", "\N{INVERTED LAZY S}"), # ‘∾’ + MacroTextSpec("wr", "\N{WREATH PRODUCT}"), # ‘≀’ + MacroTextSpec("cong", "\N{APPROXIMATELY EQUAL TO}"), # ‘≅’ + MacroTextSpec( + "approxnotequal", "\N{APPROXIMATELY BUT NOT ACTUALLY EQUAL TO}" + ), # ‘≆’ + MacroTextSpec("approxeq", "\N{ALMOST EQUAL OR EQUAL TO}"), # ‘≊’ + MacroTextSpec("tildetrpl", "\N{TRIPLE TILDE}"), # ‘≋’ + MacroTextSpec("allequal", "\N{ALL EQUAL TO}"), # ‘≌’ + MacroTextSpec("asymp", "\N{EQUIVALENT TO}"), # ‘≍’ + MacroTextSpec("Bumpeq", "\N{GEOMETRICALLY EQUIVALENT TO}"), # ‘≎’ + MacroTextSpec("bumpeq", "\N{DIFFERENCE BETWEEN}"), # ‘≏’ + MacroTextSpec("doteq", "\N{APPROACHES THE LIMIT}"), # ‘≐’ + MacroTextSpec("doteqdot", "\N{GEOMETRICALLY EQUAL TO}"), # ‘≑’ + MacroTextSpec("fallingdotseq", "\N{APPROXIMATELY EQUAL TO OR THE IMAGE OF}"), # ‘≒’ + MacroTextSpec("risingdotseq", "\N{IMAGE OF OR APPROXIMATELY EQUAL TO}"), # ‘≓’ + MacroTextSpec("eqcirc", "\N{RING IN EQUAL TO}"), # ‘≖’ + MacroTextSpec("circeq", "\N{RING EQUAL TO}"), # ‘≗’ + MacroTextSpec("estimates", "\N{ESTIMATES}"), # ‘≙’ + MacroTextSpec("starequal", "\N{STAR EQUALS}"), # ‘≛’ + MacroTextSpec("triangleq", "\N{DELTA EQUAL TO}"), # ‘≜’ + MacroTextSpec("between", "\N{BETWEEN}"), # ‘≬’ + MacroTextSpec("lessequivlnt", "\N{LESS-THAN OR EQUIVALENT TO}"), # ‘≲’ + MacroTextSpec("greaterequivlnt", "\N{GREATER-THAN OR EQUIVALENT TO}"), # ‘≳’ + MacroTextSpec("notlessgreater", "\N{NEITHER LESS-THAN NOR GREATER-THAN}"), # ‘≸’ + MacroTextSpec("notgreaterless", "\N{NEITHER GREATER-THAN NOR LESS-THAN}"), # ‘≹’ + MacroTextSpec("preccurlyeq", "\N{PRECEDES OR EQUAL TO}"), # ‘≼’ + MacroTextSpec("succcurlyeq", "\N{SUCCEEDS OR EQUAL TO}"), # ‘≽’ + MacroTextSpec("precapprox", "\N{PRECEDES OR EQUIVALENT TO}"), # ‘≾’ + MacroTextSpec("succapprox", "\N{SUCCEEDS OR EQUIVALENT TO}"), # ‘≿’ + MacroTextSpec("uplus", "\N{MULTISET UNION}"), # ‘⊎’ + MacroTextSpec("sqsubset", "\N{SQUARE IMAGE OF}"), # ‘⊏’ + MacroTextSpec("sqsupset", "\N{SQUARE ORIGINAL OF}"), # ‘⊐’ + MacroTextSpec("sqsubseteq", "\N{SQUARE IMAGE OF OR EQUAL TO}"), # ‘⊑’ + MacroTextSpec("sqsupseteq", "\N{SQUARE ORIGINAL OF OR EQUAL TO}"), # ‘⊒’ + MacroTextSpec("sqcap", "\N{SQUARE CAP}"), # ‘⊓’ + MacroTextSpec("sqcup", "\N{SQUARE CUP}"), # ‘⊔’ + MacroTextSpec("ominus", "\N{CIRCLED MINUS}"), # ‘⊖’ + MacroTextSpec("oslash", "\N{CIRCLED DIVISION SLASH}"), # ‘⊘’ + MacroTextSpec("odot", "\N{CIRCLED DOT OPERATOR}"), # ‘⊙’ + MacroTextSpec("circledcirc", "\N{CIRCLED RING OPERATOR}"), # ‘⊚’ + MacroTextSpec("circledast", "\N{CIRCLED ASTERISK OPERATOR}"), # ‘⊛’ + MacroTextSpec("circleddash", "\N{CIRCLED DASH}"), # ‘⊝’ + MacroTextSpec("boxplus", "\N{SQUARED PLUS}"), # ‘⊞’ + MacroTextSpec("boxminus", "\N{SQUARED MINUS}"), # ‘⊟’ + MacroTextSpec("boxtimes", "\N{SQUARED TIMES}"), # ‘⊠’ + MacroTextSpec("boxdot", "\N{SQUARED DOT OPERATOR}"), # ‘⊡’ + MacroTextSpec("vdash", "\N{RIGHT TACK}"), # ‘⊢’ + MacroTextSpec("dashv", "\N{LEFT TACK}"), # ‘⊣’ + MacroTextSpec("top", "\N{DOWN TACK}"), # ‘⊤’ + MacroTextSpec("perp", "\N{UP TACK}"), # ‘⊥’ + MacroTextSpec("truestate", "\N{MODELS}"), # ‘⊧’ + MacroTextSpec("forcesextra", "\N{TRUE}"), # ‘⊨’ + MacroTextSpec("Vdash", "\N{FORCES}"), # ‘⊩’ + MacroTextSpec("Vvdash", "\N{TRIPLE VERTICAL BAR RIGHT TURNSTILE}"), # ‘⊪’ + MacroTextSpec("VDash", "\N{DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE}"), # ‘⊫’ + MacroTextSpec("nvdash", "\N{DOES NOT PROVE}"), # ‘⊬’ + MacroTextSpec("nvDash", "\N{NOT TRUE}"), # ‘⊭’ + MacroTextSpec("nVdash", "\N{DOES NOT FORCE}"), # ‘⊮’ + MacroTextSpec( + "nVDash", "\N{NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE}" + ), # ‘⊯’ + MacroTextSpec("vartriangleleft", "\N{NORMAL SUBGROUP OF}"), # ‘⊲’ + MacroTextSpec("vartriangleright", "\N{CONTAINS AS NORMAL SUBGROUP}"), # ‘⊳’ + MacroTextSpec("trianglelefteq", "\N{NORMAL SUBGROUP OF OR EQUAL TO}"), # ‘⊴’ + MacroTextSpec( + "trianglerighteq", "\N{CONTAINS AS NORMAL SUBGROUP OR EQUAL TO}" + ), # ‘⊵’ + MacroTextSpec("original", "\N{ORIGINAL OF}"), # ‘⊶’ + MacroTextSpec("image", "\N{IMAGE OF}"), # ‘⊷’ + MacroTextSpec("multimap", "\N{MULTIMAP}"), # ‘⊸’ + MacroTextSpec("hermitconjmatrix", "\N{HERMITIAN CONJUGATE MATRIX}"), # ‘⊹’ + MacroTextSpec("intercal", "\N{INTERCALATE}"), # ‘⊺’ + MacroTextSpec("veebar", "\N{XOR}"), # ‘⊻’ + MacroTextSpec("rightanglearc", "\N{RIGHT ANGLE WITH ARC}"), # ‘⊾’ + MacroTextSpec("bigcap", "\N{N-ARY INTERSECTION}"), # ‘⋂’ + MacroTextSpec("bigcup", "\N{N-ARY UNION}"), # ‘⋃’ + MacroTextSpec("diamond", "\N{DIAMOND OPERATOR}"), # ‘⋄’ + MacroTextSpec("star", "\N{STAR OPERATOR}"), # ‘⋆’ + MacroTextSpec("divideontimes", "\N{DIVISION TIMES}"), # ‘⋇’ + MacroTextSpec("bowtie", "\N{BOWTIE}"), # ‘⋈’ + MacroTextSpec("ltimes", "\N{LEFT NORMAL FACTOR SEMIDIRECT PRODUCT}"), # ‘⋉’ + MacroTextSpec("rtimes", "\N{RIGHT NORMAL FACTOR SEMIDIRECT PRODUCT}"), # ‘⋊’ + MacroTextSpec("leftthreetimes", "\N{LEFT SEMIDIRECT PRODUCT}"), # ‘⋋’ + MacroTextSpec("rightthreetimes", "\N{RIGHT SEMIDIRECT PRODUCT}"), # ‘⋌’ + MacroTextSpec("backsimeq", "\N{REVERSED TILDE EQUALS}"), # ‘⋍’ + MacroTextSpec("curlyvee", "\N{CURLY LOGICAL OR}"), # ‘⋎’ + MacroTextSpec("curlywedge", "\N{CURLY LOGICAL AND}"), # ‘⋏’ + MacroTextSpec("Subset", "\N{DOUBLE SUBSET}"), # ‘⋐’ + MacroTextSpec("Supset", "\N{DOUBLE SUPERSET}"), # ‘⋑’ + MacroTextSpec("Cap", "\N{DOUBLE INTERSECTION}"), # ‘⋒’ + MacroTextSpec("Cup", "\N{DOUBLE UNION}"), # ‘⋓’ + MacroTextSpec("pitchfork", "\N{PITCHFORK}"), # ‘⋔’ + MacroTextSpec("lessdot", "\N{LESS-THAN WITH DOT}"), # ‘⋖’ + MacroTextSpec("gtrdot", "\N{GREATER-THAN WITH DOT}"), # ‘⋗’ + MacroTextSpec("verymuchless", "\N{VERY MUCH LESS-THAN}"), # ‘⋘’ + MacroTextSpec("verymuchgreater", "\N{VERY MUCH GREATER-THAN}"), # ‘⋙’ + MacroTextSpec("lesseqgtr", "\N{LESS-THAN EQUAL TO OR GREATER-THAN}"), # ‘⋚’ + MacroTextSpec("gtreqless", "\N{GREATER-THAN EQUAL TO OR LESS-THAN}"), # ‘⋛’ + MacroTextSpec("curlyeqprec", "\N{EQUAL TO OR PRECEDES}"), # ‘⋞’ + MacroTextSpec("curlyeqsucc", "\N{EQUAL TO OR SUCCEEDS}"), # ‘⋟’ + MacroTextSpec("lnsim", "\N{LESS-THAN BUT NOT EQUIVALENT TO}"), # ‘⋦’ + MacroTextSpec("gnsim", "\N{GREATER-THAN BUT NOT EQUIVALENT TO}"), # ‘⋧’ + MacroTextSpec("precedesnotsimilar", "\N{PRECEDES BUT NOT EQUIVALENT TO}"), # ‘⋨’ + MacroTextSpec("succnsim", "\N{SUCCEEDS BUT NOT EQUIVALENT TO}"), # ‘⋩’ + MacroTextSpec("ntriangleleft", "\N{NOT NORMAL SUBGROUP OF}"), # ‘⋪’ + MacroTextSpec("ntriangleright", "\N{DOES NOT CONTAIN AS NORMAL SUBGROUP}"), # ‘⋫’ + MacroTextSpec("ntrianglelefteq", "\N{NOT NORMAL SUBGROUP OF OR EQUAL TO}"), # ‘⋬’ + MacroTextSpec( + "ntrianglerighteq", "\N{DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL}" + ), # ‘⋭’ + MacroTextSpec("vdots", "\N{VERTICAL ELLIPSIS}"), # ‘⋮’ + MacroTextSpec("upslopeellipsis", "\N{UP RIGHT DIAGONAL ELLIPSIS}"), # ‘⋰’ + MacroTextSpec("downslopeellipsis", "\N{DOWN RIGHT DIAGONAL ELLIPSIS}"), # ‘⋱’ + MacroTextSpec("barwedge", "\N{PROJECTIVE}"), # ‘⌅’ + MacroTextSpec("varperspcorrespond", "\N{PERSPECTIVE}"), # ‘⌆’ + MacroTextSpec("lceil", "\N{LEFT CEILING}"), # ‘⌈’ + MacroTextSpec("rceil", "\N{RIGHT CEILING}"), # ‘⌉’ + MacroTextSpec("lfloor", "\N{LEFT FLOOR}"), # ‘⌊’ + MacroTextSpec("rfloor", "\N{RIGHT FLOOR}"), # ‘⌋’ + MacroTextSpec("recorder", "\N{TELEPHONE RECORDER}"), # ‘⌕’ + MacroTextSpec("ulcorner", "\N{TOP LEFT CORNER}"), # ‘⌜’ + MacroTextSpec("urcorner", "\N{TOP RIGHT CORNER}"), # ‘⌝’ + MacroTextSpec("llcorner", "\N{BOTTOM LEFT CORNER}"), # ‘⌞’ + MacroTextSpec("lrcorner", "\N{BOTTOM RIGHT CORNER}"), # ‘⌟’ + MacroTextSpec("frown", "\N{FROWN}"), # ‘⌢’ + MacroTextSpec("smile", "\N{SMILE}"), # ‘⌣’ + MacroTextSpec( + "lmoustache", "\N{UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION}" + ), # ‘⎰’ + MacroTextSpec( + "rmoustache", "\N{UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION}" + ), # ‘⎱’ + MacroTextSpec("textvisiblespace", "\N{OPEN BOX}"), # ‘␣’ + MacroTextSpec("circledS", "\N{CIRCLED LATIN CAPITAL LETTER S}"), # ‘Ⓢ’ + MacroTextSpec( + "diagup", "\N{BOX DRAWINGS LIGHT DIAGONAL UPPER RIGHT TO LOWER LEFT}" + ), # ‘╱’ + MacroTextSpec("square", "\N{WHITE SQUARE}"), # ‘□’ + MacroTextSpec("blacksquare", "\N{BLACK SMALL SQUARE}"), # ‘▪’ + MacroTextSpec("bigtriangleup", "\N{WHITE UP-POINTING TRIANGLE}"), # ‘△’ + MacroTextSpec("blacktriangle", "\N{BLACK UP-POINTING SMALL TRIANGLE}"), # ‘▴’ + MacroTextSpec("vartriangle", "\N{WHITE UP-POINTING SMALL TRIANGLE}"), # ‘▵’ + MacroTextSpec( + "blacktriangleright", "\N{BLACK RIGHT-POINTING SMALL TRIANGLE}" + ), # ‘▸’ + MacroTextSpec("triangleright", "\N{WHITE RIGHT-POINTING SMALL TRIANGLE}"), # ‘▹’ + MacroTextSpec("bigtriangledown", "\N{WHITE DOWN-POINTING TRIANGLE}"), # ‘▽’ + MacroTextSpec("blacktriangledown", "\N{BLACK DOWN-POINTING SMALL TRIANGLE}"), # ‘▾’ + MacroTextSpec("triangledown", "\N{WHITE DOWN-POINTING SMALL TRIANGLE}"), # ‘▿’ + MacroTextSpec("blacktriangleleft", "\N{BLACK LEFT-POINTING SMALL TRIANGLE}"), # ‘◂’ + MacroTextSpec("triangleleft", "\N{WHITE LEFT-POINTING SMALL TRIANGLE}"), # ‘◃’ + MacroTextSpec("lozenge", "\N{LOZENGE}"), # ‘◊’ + MacroTextSpec("bigcirc", "\N{WHITE CIRCLE}"), # ‘○’ + MacroTextSpec("bigcirc", "\N{LARGE CIRCLE}"), # ‘◯’ + MacroTextSpec("rightmoon", "\N{LAST QUARTER MOON}"), # ‘☾’ + MacroTextSpec("mercury", "\N{MERCURY}"), # ‘☿’ + MacroTextSpec("venus", "\N{FEMALE SIGN}"), # ‘♀’ + MacroTextSpec("male", "\N{MALE SIGN}"), # ‘♂’ + MacroTextSpec("jupiter", "\N{JUPITER}"), # ‘♃’ + MacroTextSpec("saturn", "\N{SATURN}"), # ‘♄’ + MacroTextSpec("uranus", "\N{URANUS}"), # ‘♅’ + MacroTextSpec("neptune", "\N{NEPTUNE}"), # ‘♆’ + MacroTextSpec("pluto", "\N{PLUTO}"), # ‘♇’ + MacroTextSpec("aries", "\N{ARIES}"), # ‘♈’ + MacroTextSpec("taurus", "\N{TAURUS}"), # ‘♉’ + MacroTextSpec("gemini", "\N{GEMINI}"), # ‘♊’ + MacroTextSpec("cancer", "\N{CANCER}"), # ‘♋’ + MacroTextSpec("leo", "\N{LEO}"), # ‘♌’ + MacroTextSpec("virgo", "\N{VIRGO}"), # ‘♍’ + MacroTextSpec("libra", "\N{LIBRA}"), # ‘♎’ + MacroTextSpec("scorpio", "\N{SCORPIUS}"), # ‘♏’ + MacroTextSpec("sagittarius", "\N{SAGITTARIUS}"), # ‘♐’ + MacroTextSpec("capricornus", "\N{CAPRICORN}"), # ‘♑’ + MacroTextSpec("aquarius", "\N{AQUARIUS}"), # ‘♒’ + MacroTextSpec("pisces", "\N{PISCES}"), # ‘♓’ + MacroTextSpec("diamond", "\N{WHITE DIAMOND SUIT}"), # ‘♢’ + MacroTextSpec("quarternote", "\N{QUARTER NOTE}"), # ‘♩’ + MacroTextSpec("eighthnote", "\N{EIGHTH NOTE}"), # ‘♪’ + MacroTextSpec("flat", "\N{MUSIC FLAT SIGN}"), # ‘♭’ + MacroTextSpec("natural", "\N{MUSIC NATURAL SIGN}"), # ‘♮’ + MacroTextSpec("sharp", "\N{MUSIC SHARP SIGN}"), # ‘♯’ + MacroTextSpec("longleftrightarrow", "\N{LONG LEFT RIGHT ARROW}"), # ‘⟷’ + MacroTextSpec("Longleftarrow", "\N{LONG LEFTWARDS DOUBLE ARROW}"), # ‘⟸’ + MacroTextSpec("Longrightarrow", "\N{LONG RIGHTWARDS DOUBLE ARROW}"), # ‘⟹’ + MacroTextSpec("Longleftrightarrow", "\N{LONG LEFT RIGHT DOUBLE ARROW}"), # ‘⟺’ + MacroTextSpec("longmapsto", "\N{LONG RIGHTWARDS ARROW FROM BAR}"), # ‘⟼’ + MacroTextSpec("UpArrowBar", "\N{UPWARDS ARROW TO BAR}"), # ‘⤒’ + MacroTextSpec("DownArrowBar", "\N{DOWNWARDS ARROW TO BAR}"), # ‘⤓’ + MacroTextSpec("LeftRightVector", "\N{LEFT BARB UP RIGHT BARB UP HARPOON}"), # ‘⥎’ + MacroTextSpec( + "RightUpDownVector", "\N{UP BARB RIGHT DOWN BARB RIGHT HARPOON}" + ), # ‘⥏’ + MacroTextSpec( + "DownLeftRightVector", "\N{LEFT BARB DOWN RIGHT BARB DOWN HARPOON}" + ), # ‘⥐’ + MacroTextSpec("LeftUpDownVector", "\N{UP BARB LEFT DOWN BARB LEFT HARPOON}"), # ‘⥑’ + MacroTextSpec("LeftVectorBar", "\N{LEFTWARDS HARPOON WITH BARB UP TO BAR}"), # ‘⥒’ + MacroTextSpec( + "RightVectorBar", "\N{RIGHTWARDS HARPOON WITH BARB UP TO BAR}" + ), # ‘⥓’ + MacroTextSpec( + "RightUpVectorBar", "\N{UPWARDS HARPOON WITH BARB RIGHT TO BAR}" + ), # ‘⥔’ + MacroTextSpec( + "RightDownVectorBar", "\N{DOWNWARDS HARPOON WITH BARB RIGHT TO BAR}" + ), # ‘⥕’ + MacroTextSpec( + "DownLeftVectorBar", "\N{LEFTWARDS HARPOON WITH BARB DOWN TO BAR}" + ), # ‘⥖’ + MacroTextSpec( + "DownRightVectorBar", "\N{RIGHTWARDS HARPOON WITH BARB DOWN TO BAR}" + ), # ‘⥗’ + MacroTextSpec( + "LeftUpVectorBar", "\N{UPWARDS HARPOON WITH BARB LEFT TO BAR}" + ), # ‘⥘’ + MacroTextSpec( + "LeftDownVectorBar", "\N{DOWNWARDS HARPOON WITH BARB LEFT TO BAR}" + ), # ‘⥙’ + MacroTextSpec( + "LeftTeeVector", "\N{LEFTWARDS HARPOON WITH BARB UP FROM BAR}" + ), # ‘⥚’ + MacroTextSpec( + "RightTeeVector", "\N{RIGHTWARDS HARPOON WITH BARB UP FROM BAR}" + ), # ‘⥛’ + MacroTextSpec( + "RightUpTeeVector", "\N{UPWARDS HARPOON WITH BARB RIGHT FROM BAR}" + ), # ‘⥜’ + MacroTextSpec( + "RightDownTeeVector", "\N{DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR}" + ), # ‘⥝’ + MacroTextSpec( + "DownLeftTeeVector", "\N{LEFTWARDS HARPOON WITH BARB DOWN FROM BAR}" + ), # ‘⥞’ + MacroTextSpec( + "DownRightTeeVector", "\N{RIGHTWARDS HARPOON WITH BARB DOWN FROM BAR}" + ), # ‘⥟’ + MacroTextSpec( + "LeftUpTeeVector", "\N{UPWARDS HARPOON WITH BARB LEFT FROM BAR}" + ), # ‘⥠’ + MacroTextSpec( + "LeftDownTeeVector", "\N{DOWNWARDS HARPOON WITH BARB LEFT FROM BAR}" + ), # ‘⥡’ + MacroTextSpec( + "UpEquilibrium", + "\N{UPWARDS HARPOON WITH BARB LEFT BESIDE DOWNWARDS HARPOON WITH BARB RIGHT}", + ), # ‘⥮’ + MacroTextSpec( + "ReverseUpEquilibrium", + "\N{DOWNWARDS HARPOON WITH BARB LEFT BESIDE UPWARDS HARPOON WITH BARB RIGHT}", + ), # ‘⥯’ + MacroTextSpec("RoundImplies", "\N{RIGHT DOUBLE ARROW WITH ROUNDED HEAD}"), # ‘⥰’ + MacroTextSpec("Angle", "\N{RIGHT ANGLE VARIANT WITH SQUARE}"), # ‘⦜’ + MacroTextSpec("LeftTriangleBar", "\N{LEFT TRIANGLE BESIDE VERTICAL BAR}"), # ‘⧏’ + MacroTextSpec("RightTriangleBar", "\N{VERTICAL BAR BESIDE RIGHT TRIANGLE}"), # ‘⧐’ + MacroTextSpec("blacklozenge", "\N{BLACK LOZENGE}"), # ‘⧫’ + MacroTextSpec("RuleDelayed", "\N{RULE-DELAYED}"), # ‘⧴’ + MacroTextSpec("clockoint", "\N{INTEGRAL AVERAGE WITH SLASH}"), # ‘⨏’ + MacroTextSpec("sqrint", "\N{QUATERNION INTEGRAL OPERATOR}"), # ‘⨖’ + MacroTextSpec("amalg", "\N{AMALGAMATION OR COPRODUCT}"), # ‘⨿’ + MacroTextSpec("perspcorrespond", "\N{LOGICAL AND WITH DOUBLE OVERBAR}"), # ‘⩞’ + MacroTextSpec("Equal", "\N{TWO CONSECUTIVE EQUALS SIGNS}"), # ‘⩵’ + MacroTextSpec("lessapprox", "\N{LESS-THAN OR APPROXIMATE}"), # ‘⪅’ + MacroTextSpec("gtrapprox", "\N{GREATER-THAN OR APPROXIMATE}"), # ‘⪆’ + MacroTextSpec("lneq", "\N{LESS-THAN AND SINGLE-LINE NOT EQUAL TO}"), # ‘⪇’ + MacroTextSpec("gneq", "\N{GREATER-THAN AND SINGLE-LINE NOT EQUAL TO}"), # ‘⪈’ + MacroTextSpec("lnapprox", "\N{LESS-THAN AND NOT APPROXIMATE}"), # ‘⪉’ + MacroTextSpec("gnapprox", "\N{GREATER-THAN AND NOT APPROXIMATE}"), # ‘⪊’ + MacroTextSpec( + "lesseqqgtr", "\N{LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN}" + ), # ‘⪋’ + MacroTextSpec( + "gtreqqless", "\N{GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN}" + ), # ‘⪌’ + MacroTextSpec("eqslantless", "\N{SLANTED EQUAL TO OR LESS-THAN}"), # ‘⪕’ + MacroTextSpec("eqslantgtr", "\N{SLANTED EQUAL TO OR GREATER-THAN}"), # ‘⪖’ + MacroTextSpec("NestedLessLess", "\N{DOUBLE NESTED LESS-THAN}"), # ‘⪡’ + MacroTextSpec("NestedGreaterGreater", "\N{DOUBLE NESTED GREATER-THAN}"), # ‘⪢’ + MacroTextSpec("precneqq", "\N{PRECEDES ABOVE NOT EQUAL TO}"), # ‘⪵’ + MacroTextSpec("succneqq", "\N{SUCCEEDS ABOVE NOT EQUAL TO}"), # ‘⪶’ + MacroTextSpec("precapprox", "\N{PRECEDES ABOVE ALMOST EQUAL TO}"), # ‘⪷’ + MacroTextSpec("succapprox", "\N{SUCCEEDS ABOVE ALMOST EQUAL TO}"), # ‘⪸’ + MacroTextSpec("precnapprox", "\N{PRECEDES ABOVE NOT ALMOST EQUAL TO}"), # ‘⪹’ + MacroTextSpec("succnapprox", "\N{SUCCEEDS ABOVE NOT ALMOST EQUAL TO}"), # ‘⪺’ + MacroTextSpec("subseteqq", "\N{SUBSET OF ABOVE EQUALS SIGN}"), # ‘⫅’ + MacroTextSpec("supseteqq", "\N{SUPERSET OF ABOVE EQUALS SIGN}"), # ‘⫆’ + MacroTextSpec("subsetneqq", "\N{SUBSET OF ABOVE NOT EQUAL TO}"), # ‘⫋’ + MacroTextSpec("supsetneqq", "\N{SUPERSET OF ABOVE NOT EQUAL TO}"), # ‘⫌’ + MacroTextSpec("openbracketleft", "\N{LEFT WHITE SQUARE BRACKET}"), # ‘〚’ + MacroTextSpec("openbracketright", "\N{RIGHT WHITE SQUARE BRACKET}"), # ‘〛’ +] + +# ============================================================================== + + +specs = [ + # + # CATEGORY: latex-base + # + ("latex-base", _latex_specs_base), + # + # CATEGORY: latex-approximations + # + ("latex-approximations", _latex_specs_approximations), + # + # CATEGORY: latex-placeholders + # + ("latex-placeholders", _latex_specs_placeholders), + # + # CATEGORY: nonascii-specials + # + ( + "nonascii-specials", + { + "macros": [], + "environments": [], + "specials": [ + SpecialsTextSpec("~", "\N{NO-BREAK SPACE}"), + SpecialsTextSpec("``", "\N{LEFT DOUBLE QUOTATION MARK}"), + SpecialsTextSpec("''", "\N{RIGHT DOUBLE QUOTATION MARK}"), + SpecialsTextSpec("--", "\N{EN DASH}"), + SpecialsTextSpec("---", "\N{EM DASH}"), + SpecialsTextSpec("!`", "\N{INVERTED EXCLAMATION MARK}"), + SpecialsTextSpec("?`", "\N{INVERTED QUESTION MARK}"), + ], + }, + ), + # + # CATEGORY: advanced-symbols + # + ( + "advanced-symbols", + { + "macros": advanced_symbols_macros, + "environments": [], + "specials": [], + }, + ), + # + # CATEGORY: latex-ethuebung + # + # expect these to be removed in a future version. These definitions should + # be manually included in the applications where they are relevant. + ( + "latex-ethuebung", + { + "macros": [ + MacroTextSpec("exercise", simplify_repl=_format_uebung), + MacroTextSpec("uebung", simplify_repl=_format_uebung), + MacroTextSpec("hint", "Hint: %s"), + MacroTextSpec("hints", "Hints: %s"), + MacroTextSpec("hinweis", "Hinweis: %s"), + MacroTextSpec("hinweise", "Hinweise: %s"), + ], + "environments": [], + "specials": [], + }, + ), + # + # CATEGORY: nonstandard-qit + # + # expect these to be removed in a future version. These definitions should + # be manually included in the applications where they are relevant. + ( + "nonstandard-qit", + { + "environments": [], + "specials": [], + "macros": [ + # we use these conventions as Identity operator (\mathbbm{1}) + MacroTextSpec("id", "\N{MATHEMATICAL DOUBLE-STRUCK CAPITAL I}"), + MacroTextSpec("Ident", "\N{MATHEMATICAL DOUBLE-STRUCK CAPITAL I}"), + ], + }, + ), +] + + +def _greekletters(letterlist): + for l in letterlist: + ucharname = l.upper() + if ucharname == "LAMBDA": + ucharname = "LAMDA" + smallname = "GREEK SMALL LETTER " + ucharname + if ucharname == "EPSILON": + smallname = "GREEK LUNATE EPSILON SYMBOL" + if ucharname == "PHI": + smallname = "GREEK PHI SYMBOL" + _latex_specs_base["macros"].append( + MacroTextSpec(l, unicodedata.lookup(smallname)) + ) + _latex_specs_base["macros"].append( + MacroTextSpec( + l[0].upper() + l[1:], + unicodedata.lookup("GREEK CAPITAL LETTER " + ucharname), + ) + ) + + +_greekletters( + ( + "alpha", + "beta", + "gamma", + "delta", + "epsilon", + "zeta", + "eta", + "theta", + "iota", + "kappa", + "lambda", + "mu", + "nu", + "xi", + "omicron", + "pi", + "rho", + "sigma", + "tau", + "upsilon", + "phi", + "chi", + "psi", + "omega", + ) +) +_latex_specs_base["macros"] += [ + MacroTextSpec("varepsilon", "\N{GREEK SMALL LETTER EPSILON}"), + MacroTextSpec("vartheta", "\N{GREEK THETA SYMBOL}"), + MacroTextSpec("varpi", "\N{GREEK PI SYMBOL}"), + MacroTextSpec("varrho", "\N{GREEK RHO SYMBOL}"), + MacroTextSpec("varsigma", "\N{GREEK SMALL LETTER FINAL SIGMA}"), + MacroTextSpec("varphi", "\N{GREEK SMALL LETTER PHI}"), +] + + +unicode_accents_list = ( + # see http://en.wikibooks.org/wiki/LaTeX/Special_Characters for a list + ("'", "\N{COMBINING ACUTE ACCENT}"), + ("`", "\N{COMBINING GRAVE ACCENT}"), + ('"', "\N{COMBINING DIAERESIS}"), + ("c", "\N{COMBINING CEDILLA}"), + ("^", "\N{COMBINING CIRCUMFLEX ACCENT}"), + ("~", "\N{COMBINING TILDE}"), + ("H", "\N{COMBINING DOUBLE ACUTE ACCENT}"), + ("k", "\N{COMBINING OGONEK}"), + ("=", "\N{COMBINING MACRON}"), + ("b", "\N{COMBINING MACRON BELOW}"), + (".", "\N{COMBINING DOT ABOVE}"), + ("d", "\N{COMBINING DOT BELOW}"), + ("r", "\N{COMBINING RING ABOVE}"), + ("u", "\N{COMBINING BREVE}"), + ("v", "\N{COMBINING CARON}"), + ("vec", "\N{COMBINING RIGHT ARROW ABOVE}"), + ("dot", "\N{COMBINING DOT ABOVE}"), + ("hat", "\N{COMBINING CIRCUMFLEX ACCENT}"), + ("check", "\N{COMBINING CARON}"), + ("breve", "\N{COMBINING BREVE}"), + ("acute", "\N{COMBINING ACUTE ACCENT}"), + ("grave", "\N{COMBINING GRAVE ACCENT}"), + ("tilde", "\N{COMBINING TILDE}"), + ("bar", "\N{COMBINING OVERLINE}"), + ("ddot", "\N{COMBINING DIAERESIS}"), + ("not", "\N{COMBINING LONG SOLIDUS OVERLAY}"), +) + + +def make_accented_char(node, combining, l2tobj): + if node.nodeargs and len(node.nodeargs): + nodearg = node.nodeargs[0] + c = l2tobj.nodelist_to_text([nodearg]).strip() + else: + c = " " + + def getaccented(ch, combining): + ch = unicode(ch) + combining = unicode(combining) + if ch == "\N{LATIN SMALL LETTER DOTLESS I}": + ch = "i" + if ch == "\N{LATIN SMALL LETTER DOTLESS J}": + ch = "j" + # print u"Accenting %s with %s"%(ch, combining) # this causes UnicdeDecodeError!!! + return unicodedata.normalize("NFC", unicode(ch) + combining) + + return "".join([getaccented(ch, combining) for ch in c]) + + +for u in unicode_accents_list: + (mname, mcombining) = u + _latex_specs_base["macros"].append( + MacroTextSpec( + mname, lambda x, l2tobj, c=mcombining: make_accented_char(x, c, l2tobj) + ) + ) + +# specs structure now complete diff --git a/good_ai/src/sus/external/pylatexenc/latexencode/__init__.py b/good_ai/src/sus/external/pylatexenc/latexencode/__init__.py new file mode 100644 index 0000000..06f4a61 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexencode/__init__.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +r""" +The `latexencode` module provides a set of routines that allows you to +convert a unicode string to LaTeX escape sequences. + +For basic usage you can use the :py:func:`unicode_to_latex()` function +directly:: + + >>> from pylatexenc.latexencode import unicode_to_latex + >>> print(unicode_to_latex('À votre santé')) + \`A votre sant\'e + >>> print(unicode_to_latex('The length of samples #3 & #4 is 3μm')) + The length of samples \#3 \& \#4 is 3\ensuremath{\mu}m + +The conversion is handled by the class :py:class:`UnicodeToLatexEncoder`. If +you are converting multiple strings, you may create an instance with the flags +you like and invoke its method +:py:meth:`~UnicodeToLatexEncoder.unicode_to_latex()` as many times as necessary:: + + >>> from pylatexenc.latexencode import UnicodeToLatexEncoder + >>> u = UnicodeToLatexEncoder(unknown_char_policy='replace') + >>> print(u.unicode_to_latex('À votre santé')) + \`A votre sant\'e + >>> print(u.unicode_to_latex('The length of samples #3 & #4 is 3μm')) + The length of samples \#3 \& \#4 is 3\ensuremath{\mu}m + >>> print(u.unicode_to_latex('À votre santé: 乾杯')) + No known latex representation for character: U+4E7E - ‘乾’ + No known latex representation for character: U+676F - ‘杯’ + \`A votre sant\'e: {\bfseries ?}{\bfseries ?} + +Example using custom conversion rules:: + + >>> from pylatexenc.latexencode import UnicodeToLatexEncoder, \ + ... UnicodeToLatexConversionRule, RULE_REGEX + >>> u = UnicodeToLatexEncoder( + ... conversion_rules=[ + ... UnicodeToLatexConversionRule(rule_type=RULE_REGEX, rule=[ + ... (re.compile(r'-->'), r'\\textrightarrow'), + ... (re.compile(r'<--'), r'\\textleftarrow'), + ... ]), + ... 'defaults' + ... ] + ... ) + >>> print(u.unicode_to_latex("Cheers --> À votre santé")) + Cheers {\textrightarrow} \`A votre sant\'e + +See :py:class:`UnicodeToLatexEncoder` and +:py:class:`UnicodeToLatexConversionRule`. Note for regex rules, the replacement +text is expanded like the second argument of `re.sub()` and backslashes need to +be escaped even inside raw strings. + +.. versionadded:: 2.0 + + The class :py:class:`UnicodeToLatexEncoder` along with its helper functions + and classes were introduced in `pylatexenc 2.0`. + + The earlier function :py:func:`utf8tolatex()` that was available in + `pylatexenc 1.x` is still provided unchanged, so code written for `pylatexenc + 1.x` should work without changes. New code is however strongly encouraged to + employ the new API. +""" + +from __future__ import absolute_import, print_function, unicode_literals + +import functools +import itertools +import logging +import sys +import unicodedata + +if sys.version_info.major > 2: + unicode = str # need to support unicode() w/ no arguments + basestring = str + # use MappingProxyType for keeping + # inspect function argument names + from inspect import getfullargspec + from types import MappingProxyType as _MappingProxyType +else: + _MappingProxyType = dict + # inspect function argument names -- simulate getfullargspec with getargspec (argh...) + from inspect import getargspec as getfullargspec + +logger = logging.getLogger(__name__) + + +from .. import _util +from ._partial_latex_encoder import PartialLatexToLatexEncoder +from ._unicode_to_latex_encoder import ( + RULE_CALLABLE, + RULE_DICT, + RULE_REGEX, + UnicodeToLatexConversionRule, + UnicodeToLatexEncoder, + get_builtin_conversion_rules, + get_builtin_uni2latex_dict, +) + +# ------------------------------------------------ + + +# ------------------------------------------------ + + +# ------------------------------------------------ + + +_u2l_obj_cache = {} + + +def unicode_to_latex( + s, + non_ascii_only=False, + replacement_latex_protection="braces", + unknown_char_policy="keep", + unknown_char_warning=True, +): + r""" + Shorthand for constructing a :py:class:`UnicodeToLatexEncoder` instance and + calling its :py:meth:`~UnicodeToLatexEncoder.unicode_to_latex()` method. + + The :py:class:`UnicodeToLatexEncoder` instances for given option settings + are cached, making repeated calls to :py:func:`unicode_to_latex()` possible + without creating a new instance upon each call. + + The parameters `non_ascii_only`, `replacement_latex_protection`, + `unknown_char_policy`, and `unknown_char_warning` are directly passed on to + the :py:class:`UnicodeToLatexEncoder` constructor. See the class doc for + :py:class:`UnicodeToLatexEncoder` for more information about what they do. + + You may only use arguments to this function that are python hashable (like + `True`, `False`, or simple strings) to help us keep a cache of previously + constructed :py:class:`UnicodeToLatexEncoder` instances. For instance, it + is not possible to provide a callable to `unknown_char_policy`. It is also + not possible to specify custom conversion rules with this helper function. + If you need any of these features, simply create a + :py:class:`UnicodeToLatexEncoder` instance directly. + """ + + key = ( + non_ascii_only, + replacement_latex_protection, + unknown_char_policy, + unknown_char_warning, + ) + + if key in _u2l_obj_cache: + u = _u2l_obj_cache[key] + else: + u = UnicodeToLatexEncoder( + non_ascii_only=non_ascii_only, + replacement_latex_protection=replacement_latex_protection, + unknown_char_policy=unknown_char_policy, + unknown_char_warning=unknown_char_warning, + ) + _u2l_obj_cache[key] = u + + return u.unicode_to_latex(s) + + +# ------------------------------------------------------------------------------ + +# Don't change pylatexenc 1.x function: + + +def _get_deprecated_utf82latex(): + # + # Don't issue a deprecation warning, because utf8tolatex() uses the + # `utf82latex` dict even if it isn't modified by the user. + # + # _util.pylatexenc_deprecated_2( + # "The module-level dictionary `pylatexenc.latexencode.utf82latex` is deprecated " + # "and might be removed in a future version of `pylatexenc`.", + # ) + + # return a copy of the dict so that the user can modify the module-level + # `utf82latex` dict without influencing the behavior of the new + # `unicode_to_latex()` routines. (E.g., if two python modules use + # pylatexenc.latexencode, we don't want one python module's use of + # `utf2tolatex()` to influence the behavior of another module's use of + # `unicode_to_latex()`. If both modules use `utf8tolatex()`, we can't avoid + # this influence.) + from ._uni2latexmap import uni2latex as _uni2latex + + return _uni2latex.copy() + + +utf82latex = _util.LazyDict(generate_dict_fn=_get_deprecated_utf82latex) +""" +.. deprecated:: 2.0 + + Pylatexenc 1.x exposed the module-level dictionary `utf82latex` that could be + modified to alter the behavior of `utf8tolatex()`. + + If you would like to obtain a copy of the built-in unicode to text + dictionary, see :py:func:`get_builtin_uni2latex_dict()`. If you would like + to alter the behavior of :py:func:`utf8tolatex()`, you should use + :py:class:`UnicodeToLatexEncoder` which provides a rich interface for + specifying rules how to convert chars to LaTeX escapes. + + For backwards compatibility, you can still modify the module-level dictionary + `utf82latex` (but you can't assign a new object to it) and this will directly + modify the global built-in dictionary of known latex escapes. This is not + recommended however, and the `utf82latex` module-level dictionary might be + removed in the future. + + .. warning:: + + Modifying the `utf82latex` module-level dictionary is not recommended. + Doing so will alter the behavior of the `utf8tolatex()` function also for + all other modules that also use `pylatexenc`! +""" + + +def utf8tolatex( + s, + non_ascii_only=False, + brackets=True, + substitute_bad_chars=False, + fail_bad_chars=False, +): + """ + .. note:: + + Since `pylatexenc 2.0`, it is recommended to use the the + :py:func:`unicode_to_latex()` function or the + :py:class:`UnicodeToLatexEncoder` class instead of the earlier function + `utf8tolatex()`. + + The new routines provide much more flexibility and versatility. For + instance, you can specify custom escape sequences for certain characters. + Some cheap benchmarks seem to indicate that the new routines are not + significantly slower than the `utf8tolatex()` function. Also, the name + `utf8tolatex()` was poorly chosen, since the argument is in fact not + 'utf-8'-encoded but rather a Python unicode string object. + + The function `utf8tolatex()` is still provided unchanged from `pylatexenc + 1.x`. We do not plan to remove this function in the near future so it is + not (yet) considered as deprecated and we will continue to provide it in + near future versions of `pylatexenc`. Bug reports, improvements, and new + features will however be directed to :py:func:`UnicodeToLatexEncoder()`. + + Encode a UTF-8 string to a LaTeX snippet. + + If `non_ascii_only` is set to `True`, then usual (ascii) characters such as ``#``, + ``{``, ``}`` etc. will not be escaped. If set to `False` (the default), they are + escaped to their respective LaTeX escape sequences. + + If `brackets` is set to `True` (the default), then LaTeX macros are enclosed in + brackets. For example, ``sant\N{LATIN SMALL LETTER E WITH ACUTE}`` is replaced by + ``sant{\\'e}`` if `brackets=True` and by ``sant\\'e`` if `brackets=False`. + + .. warning:: + Using `brackets=False` might give you an invalid LaTeX string, so avoid + it! (for instance, ``ma\N{LATIN SMALL LETTER I WITH CIRCUMFLEX}tre`` will be + replaced incorrectly by ``ma\\^\\itre`` resulting in an unknown macro ``\\itre``). + + If `substitute_bad_chars=True`, then any non-ascii character for which no LaTeX escape + sequence is known is replaced by a question mark in boldface. Otherwise (by default), + the character is left as it is. + + If `fail_bad_chars=True`, then a `ValueError` is raised if we cannot find a + character substitution for any non-ascii character. + + .. versionchanged:: 1.3 + + Added `fail_bad_chars` switch + """ + + s = unicode(s) # make sure s is unicode + s = unicodedata.normalize("NFC", s) + + if not s: + return "" + + result = "" + for ch in s: + # logger.longdebug("Encoding char %r", ch) + if non_ascii_only and ord(ch) < 127: + result += ch + else: + # use the `utf82latex` dict -- not `_uni2latex` which should NOT be + # modified externally even for backwards-compatible code + lch = utf82latex.get(ord(ch), None) + if lch is not None: + # add brackets if needed, i.e. if we have a substituting macro. + # note: in condition, beware, that lch might be of zero length. + result += "{" + lch + "}" if brackets and lch[0:1] == "\\" else lch + elif (ord(ch) >= 32 and ord(ch) <= 127) or (ch in "\n\r\t"): + # ordinary printable ascii char, just add it + result += ch + else: + # non-ascii char + msg = "Character cannot be encoded into LaTeX: U+%04X - `%s'" % ( + ord(ch), + ch, + ) + if fail_bad_chars: + raise ValueError(msg) + + logger.warning(msg) + if substitute_bad_chars: + result += r"{\bfseries ?}" + else: + # keep unescaped char + result += ch + + return result diff --git a/good_ai/src/sus/external/pylatexenc/latexencode/__main__.py b/good_ai/src/sus/external/pylatexenc/latexencode/__main__.py new file mode 100644 index 0000000..d770795 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexencode/__main__.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2019 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +import argparse +import fileinput +import logging +import sys + +from ..latexencode import unicode_to_latex +from ..version import version_str + + +def main(argv=None): + + if argv is None: + argv = sys.argv[1:] + + parser = argparse.ArgumentParser(prog="latexencode", add_help=False) + parser.add_argument( + "files", + metavar="FILE", + nargs="*", + help="Input files (if none specified, read from stdandard input)", + ) + + parser.add_argument( + "--non-ascii-only", + action="store_const", + const=True, + dest="non_ascii_only", + default=False, + ) + parser.add_argument( + "--no-non-ascii-only", + action="store_const", + const=False, + dest="non_ascii_only", + help="The option --non-ascii-only specifies that only non-ascii characters " + "are to be encoded into LaTeX sequences, and not characters like '$' " + "even though they might have a special LaTeX meaning.", + ) + + parser.add_argument( + "--replacement-latex-protection", + choices=( + "braces", + "braces-all", + "braces-almost-all", + "braces-after-macro", + "none", + ), + dest="replacement_latex_protection", + default="braces", + help=r"How to protect replacement latex code from producing invalid latex code " + r"when concatenated in a longer string. One of 'braces', 'braces-all', " + r"'braces-almost-all', 'braces-after-macro', 'none'. Example: using " + r"choice 'braces' we avoid the invalid replacement 'a→b' -> 'a\tob' " + r"with instead 'a{\to}b'.", + ) + + parser.add_argument( + "--unknown-char-policy", + choices=("keep", "replace", "ignore", "fail"), + dest="unknown_char_policy", + default="keep", + help="How to deal with nonascii characters with no known latex code equivalent.", + ) + + parser.add_argument( + "-q", + "--quiet", + dest="logging_level", + action="store_const", + const=logging.ERROR, + default=logging.INFO, + help="Suppress warning messages", + ) + parser.add_argument( + "--version", + action="version", + version="pylatexenc {}".format(version_str), + help="Show version information and exit", + ) + parser.add_argument( + "--help", action="help", help="Show this help information and exit" + ) + + args = parser.parse_args(argv) + + logging.basicConfig() + logging.getLogger().setLevel(args.logging_level) + + latex = "" + for line in fileinput.input(files=args.files): + latex += line + + result = unicode_to_latex( + latex, + non_ascii_only=args.non_ascii_only, + replacement_latex_protection=args.replacement_latex_protection, + unknown_char_policy=args.unknown_char_policy, + ) + + sys.stdout.write(result) + + +def run_main(): + try: + + main() + + except SystemExit: + raise + except: # lgtm [py/catch-base-exception] + import pdb + import traceback + + traceback.print_exc() + pdb.post_mortem() + + +if __name__ == "__main__": + + # run_main() ## DEBUG + main() diff --git a/good_ai/src/sus/external/pylatexenc/latexencode/_partial_latex_encoder.py b/good_ai/src/sus/external/pylatexenc/latexencode/_partial_latex_encoder.py new file mode 100644 index 0000000..393551d --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexencode/_partial_latex_encoder.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2021 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from __future__ import absolute_import, print_function, unicode_literals + +# import sys +import logging + +logger = logging.getLogger(__name__) + + +from ._unicode_to_latex_encoder import ( + RULE_CALLABLE, + UnicodeToLatexConversionRule, + UnicodeToLatexEncoder, +) + +# if sys.version_info.major == 2: +# bytes = str +# str = unicode + + +class PartialLatexToLatexEncoder(UnicodeToLatexEncoder): + r""" + Encode a string while preserving some (fuzzily detected) LaTeX constructs + that the input string already has (e.g. accent macros or inline math modes). + + Sometimes you need to fully LaTeX-encode a string that already has some + LaTeX constructs. For instance, titles of bibliographic entries might + include some inline math or accents, but they might also include unicode + characters that need to be encoded. Using a + :py:class:`UnicodeToLatexEncoder` on such strings would result in ugly + doubly-escaped strings such as ``\textbackslash{}'\{e\}``. Instead, + constructs such as ``\'{e}`` should be preserved while other characters + and/or constructs (say '&' or '%') as well as unicode characters should be + encoded. + + This class offers a simple partial solution: Characters are encoded as per + the given `conversion_rules` (or the default conversion rules of + :py:class:`UnicodeToLatexEncoder` objects), except that the characters in + `keep_latex_chars` are to be interpreted as LaTeX and are not to be further + encoded. + + .. versionadded: 2.10 + """ + + def __init__( + self, + # keyword arguments: + keep_latex_chars=r"\${}^_", + conversion_rules=None, + **kwargs + ): + + base_conversion_rules = conversion_rules + if base_conversion_rules is None: + base_conversion_rules = ["defaults"] + + super(PartialLatexToLatexEncoder, self).__init__( + # only a single rule, our own special method that tries to parse + # partial latex. + conversion_rules=[ + UnicodeToLatexConversionRule( + rule_type=RULE_CALLABLE, + rule=self._do_partial_latex_encode_step, + replacement_latex_protection="none", + ) + ] + + base_conversion_rules, + **kwargs + ) + + self.keep_latex_chars = keep_latex_chars + + def _do_partial_latex_encode_step(self, s, pos): + r""" + This method is used as a "callable rule" for the + :py:class:`UnicodeToLatexEncoder` object. + + The strategy is to see if we have something that looks like a LaTeX char + we want to keep. If so, keep it as is; if not, return `None` so that + further rules can be considered by the base unicode encoder. + """ + + from ..latexwalker import LatexWalker + + if s[pos] in self.keep_latex_chars: + # Read a token and if it is a macro, keep the full macro! + lw = LatexWalker(s, tolerant_parsing=False) + + tok = lw.get_token(pos, environments=False) + + tok_as_latex = tok.pre_space + s[tok.pos : tok.pos + tok.len] + + # keep the LaTeX token as-is + return (tok.pos + tok.len - pos, tok_as_latex) + + return None diff --git a/good_ai/src/sus/external/pylatexenc/latexencode/_uni2latexmap.py b/good_ai/src/sus/external/pylatexenc/latexencode/_uni2latexmap.py new file mode 100644 index 0000000..7fcf1a2 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexencode/_uni2latexmap.py @@ -0,0 +1,1590 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2015 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +# +# CHARACTER MAP ADAPTED FROM: +# +# latexcodec 0.2, by Peter Troeger +# https://pypi.python.org/pypi/latexcodec +# +# +# latexcodec is a lexer and codec to work with LaTeX code in Python +# Copyright (c) 2011-2014 by Matthias C. M. Troffaes +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# + + +# generated by genmapping.py ### PhF: ... and edited manually by myself +uni2latex = { + 0x0022: "''", # character " + 0x0023: r"\#", # character # + 0x0024: r"\$", # character $ + 0x0025: r"\%", # character % + 0x0026: r"\&", # character & + 0x003C: r"\ensuremath{<}", # < + 0x003E: r"\ensuremath{>}", # > + 0x005C: r"\textbackslash", # the \ character itself + 0x005E: r"\textasciicircum", # character ^ + 0x005F: r"\_", # character _ + 0x007B: r"\{", # character { + 0x007D: r"\}", # character } + 0x007E: r"\textasciitilde", # character ~ + 0x00A0: r"~", # character NO-BREAK SPACE + 0x00A1: r"\textexclamdown", # character ¡ + 0x00A2: r"\textcent", # character ¢ + 0x00A3: r"\textsterling", # character £ + 0x00A4: r"\textcurrency", # character ¤ + 0x00A5: r"\textyen", # character ¥ + 0x00A6: r"\textbrokenbar", # character ¦ + 0x00A7: r"\textsection", # character § + 0x00A8: r"\textasciidieresis", # character ¨ + 0x00A9: r"\textcopyright", # character © + 0x00AA: r"\textordfeminine", # character ª + 0x00AB: r"\guillemotleft", # character « + 0x00AC: r"\textlnot", # character ¬ + 0x00AD: r"\-", # SOFT HYPHEN [­] + 0x00AE: r"\textregistered", # character ® + 0x00AF: r"\textasciimacron", # character ¯ + 0x00B0: r"\textdegree", # character ° + 0x00B1: r"\ensuremath{\pm}", # character ± + 0x00B2: r"\texttwosuperior", # character ² + 0x00B3: r"\textthreesuperior", # character ³ + 0x00B4: r"\textasciiacute", # character ´ + 0x00B5: r"\textmu", # character µ + 0x00B6: r"\textparagraph", # character ¶ + 0x00B7: r"\textperiodcentered", # character · + 0x00B9: r"\textonesuperior", # character ¹ + 0x00BA: r"\textordmasculine", # character º + 0x00BB: r"\guillemotright", # character » + 0x00BC: r"\textonequarter", # character ¼ + 0x00BD: r"\textonehalf", # character ½ + 0x00BE: r"\textthreequarters", # character ¾ + 0x00BF: r"\textquestiondown", # character ¿ + 0x00C0: r"\`A", # character À + 0x00C1: "\\'A", # character Á + 0x00C2: r"\^A", # character  + 0x00C3: r"\~A", # character à + 0x00C4: r"\"A", # character Ä + 0x00C5: r"\r{A}", # character Å + 0x00C6: r"\AE", # character Æ + 0x00C7: r"\c{C}", # character Ç + 0x00C8: r"\`E", # character È + 0x00C9: "\\'E", # character É + 0x00CA: r"\^E", # character Ê + 0x00CB: r"\"E", # character Ë + 0x00CC: r"\`I", # character Ì + 0x00CD: "\\'I", # character Í + 0x00CE: r"\^I", # character Î + 0x00CF: r"\"I", # character Ï + 0x00D0: r"\DH", # character Ð + 0x00D1: r"\~N", # character Ñ + 0x00D2: r"\`O", # character Ò + 0x00D3: "\\'O", # character Ó + 0x00D4: r"\^O", # character Ô + 0x00D5: r"\~O", # character Õ + 0x00D6: r"\"O", # character Ö + 0x00D7: r"\texttimes", # character × + 0x00D8: r"\O", # character Ø + 0x00D9: r"\`U", # character Ù + 0x00DA: "\\'U", # character Ú + 0x00DB: r"\^U", # character Û + 0x00DC: r"\"U", # character Ü + 0x00DD: "\\'Y", # character Ý + 0x00DE: r"\TH", # character Þ + 0x00DF: r"\ss", # character ß + 0x00E0: r"\`a", # character à + 0x00E1: "\\'a", # character á + 0x00E2: r"\^a", # character â + 0x00E3: r"\~a", # character ã + 0x00E4: r"\"a", # character ä + 0x00E5: r"\r{a}", # character å + 0x00E6: r"\ae", # character æ + 0x00E7: r"\c{c}", # character ç + 0x00E8: r"\`e", # character è + 0x00E9: "\\'e", # character é + 0x00EA: r"\^e", # character ê + 0x00EB: r"\"e", # character ë + 0x00EC: r"\`\i", # character ì + 0x00ED: "\\'\\i", # character í + 0x00EE: r"\^\i", # character î + 0x00EF: r"\"\i", # character ï + 0x00F0: r"\dh", # character ð + 0x00F1: r"\~n", # character ñ + 0x00F2: r"\`o", # character ò + 0x00F3: "\\'o", # character ó + 0x00F4: r"\^o", # character ô + 0x00F5: r"\~o", # character õ + 0x00F6: r"\"o", # character ö + 0x00F7: r"\textdiv", # character ÷ + 0x00F8: r"\o", # character ø + 0x00F9: r"\`u", # character ù + 0x00FA: "\\'u", # character ú + 0x00FB: r"\^u", # character û + 0x00FC: r"\"u", # character ü + 0x00FD: "\\'y", # character ý + 0x00FE: r"\th", # character þ + 0x00FF: r"\"y", # character ÿ + 0x0100: r"\={A}", + 0x0101: r"\={a}", + 0x0102: r"\u{A}", + 0x0103: r"\u{a}", + 0x0104: r"\k{A}", + 0x0105: r"\k{a}", + 0x0106: "\\'C", + 0x0107: "\\'c", + 0x0108: r"\^{C}", + 0x0109: r"\^{c}", + 0x010A: r"\.{C}", + 0x010B: r"\.{c}", + 0x010C: r"\v{C}", + 0x010D: r"\v{c}", + 0x010E: r"\v{D}", + 0x010F: r"\v{d}", + 0x0110: r"\DJ", + 0x0111: r"\dj", + 0x0112: r"\={E}", + 0x0113: r"\={e}", + 0x0114: r"\u{E}", + 0x0115: r"\u{e}", + 0x0116: r"\.{E}", + 0x0117: r"\.{e}", + 0x0118: r"\k{E}", + 0x0119: r"\k{e}", + 0x011A: r"\v{E}", + 0x011B: r"\v{e}", + 0x011C: r"\^{G}", + 0x011D: r"\^{g}", + 0x011E: r"\u{G}", + 0x011F: r"\u{g}", + 0x0120: r"\.{G}", + 0x0121: r"\.{g}", + 0x0122: r"\c{G}", + 0x0123: r"\c{g}", + 0x0124: r"\^{H}", + 0x0125: r"\^{h}", + 0x0126: r"\={H}", + 0x0127: r"\={h}", + 0x0128: r"\~{I}", + 0x0129: r"\~{i}", + 0x012A: r"\={I}", + 0x012B: r"\={i}", + 0x012C: r"\u{I}", + 0x012D: r"\u{i}", + 0x012E: r"\k{I}", + 0x012F: r"\k{i}", + 0x0130: r"\.I", + 0x0131: r"\i", + 0x0132: r"\IJ", + 0x0133: r"\ij", + 0x0134: r"\^{J}", + 0x0135: r"\^{j}", + 0x0136: r"\c{K}", + 0x0137: r"\c{k}", + 0x0138: r"\textsc{k}", + 0x0139: "\\'L", + 0x013A: "\\'l", + 0x013B: r"\c{L}", + 0x013C: r"\c{l}", + 0x013D: r"\v{L}", + 0x013E: r"\v{l}", + 0x013F: r"\.{L}", + 0x0140: r"\.{l}", + 0x0141: r"\L", + 0x0142: r"\l", + 0x0143: "\\'N", + 0x0144: "\\'n", + 0x0145: r"\c{N}", + 0x0146: r"\c{n}", + 0x0147: r"\v{N}", + 0x0148: r"\v{n}", + 0x0149: r"\nument{149}", + 0x014A: r"\NG", + 0x014B: r"\ng", + 0x014C: r"\={O}", + 0x014D: r"\={o}", + 0x014E: r"\u{O}", + 0x014F: r"\u{o}", + 0x0150: r"\H{O}", + 0x0151: r"\H{o}", + 0x0152: r"\OE", + 0x0153: r"\oe", + 0x0154: "\\'R", + 0x0155: "\\'r", + 0x0156: r"\c{R}", + 0x0157: r"\c{r}", + 0x0158: r"\v{R}", + 0x0159: r"\v{r}", + 0x015A: "\\'S", + 0x015B: "\\'s", + 0x015C: r"\^{S}", + 0x015D: r"\^{s}", + 0x015E: r"\c{S}", + 0x015F: r"\c{s}", + 0x0160: r"\v{S}", + 0x0161: r"\v{s}", + 0x0162: r"\c{T}", + 0x0163: r"\c{t}", + 0x0164: r"\v{T}", + 0x0165: r"\v{t}", + 0x0166: r"\={T}", + 0x0167: r"\={t}", + 0x0168: r"\~{U}", + 0x0169: r"\~{u}", + 0x016A: r"\={U}", + 0x016B: r"\={u}", + 0x016C: r"\u{U}", + 0x016D: r"\u{u}", + 0x016E: r"\r{U}", + 0x016F: r"\r{u}", + 0x0170: "\\'{U}", + 0x0171: "\\'{u}", + 0x0172: r"\k{U}", + 0x0173: r"\k{u}", + 0x0174: r"\^{W}", + 0x0175: r"\^{w}", + 0x0176: r"\^{Y}", + 0x0177: r"\^{y}", + 0x0178: r"\"Y", + 0x0179: "\\'Z", + 0x017A: "\\'z", + 0x017B: r"\.Z", + 0x017C: r"\.z", + 0x017D: r"\v{Z}", + 0x017E: r"\v{z}", + 0x0192: r"\textflorin", # 0x0192 + 0x0195: r"\texthvlig", # LATIN SMALL LETTER HV [ƕ] + 0x019E: r"\textnrleg", # LATIN SMALL LETTER N WITH LONG RIGHT LEG [ƞ] + 0x01F5: r"\'{g}", # LATIN SMALL LETTER G WITH ACUTE [ǵ] + 0x0228: r"\c{E}", + 0x0229: r"\c{e}", + # chars in linguistics, thanks @roedoejet (https://github.com/roedoejet/pylatexenc) + 0x0259: r"\textschwa", + 0x025B: r"\varepsilon", # LATIN SMALL LETTER OPEN E [ɛ] + 0x0278: r"\textphi", # LATIN SMALL LETTER PHI [ɸ] + 0x0294: r"\textglotstop", + 0x029E: r"\textturnk", # LATIN SMALL LETTER TURNED K [ʞ] + 0x02B7: r"\textsuperscript{w}", + 0x02C6: r"\textasciicircum", # 0x02C6 + 0x02C7: r"\textasciicaron", + 0x02D8: r"\textasciibreve", + 0x02D9: r"\textperiodcentered", # DOT ABOVE [˙] + 0x02DA: r"\r{}", # RING ABOVE [˚] + 0x02DB: r"\k{}", # OGONEK [˛] + 0x02DC: r"\textasciitilde", + 0x02DD: r"\textacutedbl", # 0x02DD + # --------------------- + 0x02BC: "'", # MODIFIER LETTER APOSTROPHE + # Combining Diacritical Marks (!!TODO!! smarter) + 0x0307: r"\ensuremath{\dot{}}", + 0x0308: r"\ensuremath{\ddot{}}", + 0x0386: "\\'{}A", # GREEK CAPITAL LETTER ALPHA WITH TONOS [Ά] + 0x0388: "\\'{}E", # GREEK CAPITAL LETTER EPSILON WITH TONOS [Έ] + 0x0389: "\\'{}H", # GREEK CAPITAL LETTER ETA WITH TONOS [Ή] + 0x038A: "\\'{}I", # GREEK CAPITAL LETTER IOTA WITH TONOS [Ί] + 0x038C: "\\'{}O", # GREEK CAPITAL LETTER OMICRON WITH TONOS [Ό] + 0x038E: "\\'{}Y", # GREEK CAPITAL LETTER UPSILON WITH TONOS [Ύ] + 0x038F: "\\'{}\\ensuremath{\\Omega}", # GREEK CAPITAL LETTER OMEGA WITH TONOS [Ώ] + 0x0390: r"\acute{\ddot{\iota}}", # GREEK SMALL LETTER IOTA WITH DIALYTIKA AND TONOS [ΐ] + 0x0391: r"A", # GREEK CAPITAL LETTER ALPHA + 0x0392: r"B", # GREEK CAPITAL LETTER BETA + 0x0393: r"\ensuremath{\Gamma}", # GREEK CAPITAL LETTER GAMMA + 0x0394: r"\ensuremath{\Delta}", # ... + 0x0395: r"E", + 0x0396: r"Z", + 0x0397: r"H", + 0x0398: r"\ensuremath{\Theta}", + 0x0399: r"I", + 0x039A: r"K", + 0x039B: r"\ensuremath{\Lambda}", + 0x039C: r"M", + 0x039D: r"N", + 0x039E: r"\ensuremath{\Xi}", + 0x039F: r"O", + 0x03A0: r"\ensuremath{\Pi}", + 0x03A1: r"P", + 0x03A3: r"\ensuremath{\Sigma}", + 0x03A4: r"T", + 0x03A5: r"\ensuremath{\Upsilon}", + 0x03A6: r"\ensuremath{\Phi}", + 0x03A7: r"X", + 0x03A8: r"\ensuremath{\Psi}", + 0x03A9: r"\ensuremath{\Omega}", + # tonos letters [ ... ] + 0x03AA: r"\ensuremath{\ddot{I}}", # GREEK CAPITAL LETTER IOTA WITH DIALYTIKA [Ϊ] + 0x03AB: r"\ensuremath{\ddot{Y}}", # GREEK CAPITAL LETTER UPSILON WITH DIALYTIKA [Ϋ] + 0x03AC: r"\ensuremath{\acute\alpha}", # GREEK SMALL LETTER ALPHA WITH TONOS [ά] + 0x03AD: r"\ensuremath{\acute\epsilon}", # GREEK SMALL LETTER EPSILON WITH TONOS [έ] + 0x03AE: r"\ensuremath{\acute\eta}", # GREEK SMALL LETTER ETA WITH TONOS [ή] + 0x03AF: r"\ensuremath{\acute\iota}", # GREEK SMALL LETTER IOTA WITH TONOS [ί] + 0x03B0: r"\ensuremath{\acute{\ddot{\upsilon}}}", # GREEK SMALL LETTER UPSILON WITH DIALYTIKA AND TONOS [ΰ] + 0x03CA: r"\ensuremath{\ddot\iota}", # GREEK SMALL LETTER IOTA WITH DIALYTIKA [ϊ] + 0x03CB: r"\ensuremath{\ddot{\upsilon}}", # GREEK SMALL LETTER UPSILON WITH DIALYTIKA [ϋ] + 0x03CC: r"\'{o}", # GREEK SMALL LETTER OMICRON WITH TONOS [ό] + 0x03CD: r"\ensuremath{\acute\upsilon}", # GREEK SMALL LETTER UPSILON WITH TONOS [ύ] + 0x03CE: r"\ensuremath{\acute\omega}", # GREEK SMALL LETTER OMEGA WITH TONOS [ώ] + 0x03B1: r"\ensuremath{\alpha}", # Greek Small Letter Alpha + 0x03B2: r"\ensuremath{\beta}", + 0x03B3: r"\ensuremath{\gamma}", + 0x03B4: r"\ensuremath{\delta}", + 0x03B5: r"\ensuremath{\varepsilon}", + 0x03B6: r"\ensuremath{\zeta}", + 0x03B7: r"\ensuremath{\eta}", + 0x03B8: r"\ensuremath{\theta}", + 0x03B9: r"\ensuremath{\iota}", + 0x03BA: r"\ensuremath{\kappa}", + 0x03BB: r"\ensuremath{\lambda}", + 0x03BC: r"\ensuremath{\mu}", + 0x03BD: r"\ensuremath{\nu}", + 0x03BE: r"\ensuremath{\xi}", + 0x03BF: r"o", + 0x03C0: r"\ensuremath{\pi}", + 0x03C1: r"\ensuremath{\rho}", + 0x03C2: r"\ensuremath{\varsigma}", + 0x03C3: r"\ensuremath{\sigma}", + 0x03C4: r"\ensuremath{\tau}", + 0x03C5: r"\ensuremath{\upsilon}", + 0x03C6: r"\ensuremath{\varphi}", + 0x03C7: r"\ensuremath{\chi}", + 0x03C8: r"\ensuremath{\psi}", + 0x03C9: r"\ensuremath{\omega}", + 0x03D1: r"\ensuremath{\vartheta}", # GREEK THETA SYMBOL [ϑ] + 0x03D2: r"\Upsilon", # GREEK UPSILON WITH HOOK SYMBOL [ϒ] + 0x03D5: r"\ensuremath{\phi}", # GREEK PHI SYMBOL [ϕ] + 0x03D6: r"\ensuremath{\varpi}", # GREEK PI SYMBOL [ϖ] + 0x03F0: r"\ensuremath{\varkappa}", # GREEK KAPPA SYMBOL [ϰ] + 0x03F1: r"\ensuremath{\varrho}", # GREEK RHO SYMBOL [ϱ] + 0x03F5: r"\ensuremath{\epsilon}", # GREEK LUNATE EPSILON SYMBOL [ϵ] + 0x03F6: r"\ensuremath{\backepsilon}", # GREEK REVERSED LUNATE EPSILON SYMBOL [϶] + 0x0400: r"\`\CYRE", # 0x0400 + 0x0401: r"\CYRYO", + 0x0402: r"\CYRDJE", + 0x0403: r"\`\CYRG", + 0x0404: r"\CYRIE", + 0x0405: r"\CYRDZE", + 0x0406: r"\CYRII", + 0x0407: r"\CYRYI", + 0x0408: r"\CYRJE", + 0x0409: r"\CYRLJE", + 0x040A: r"\CYRNJE", + 0x040B: r"\CYRTSHE", + 0x040C: r"\`\CYRK", + 0x040D: r"\`\CYRI", + 0x040E: r"\CYRUSHRT", + 0x040F: r"\CYRDZHE", + 0x0410: r"\CYRA", + 0x0411: r"\CYRB", + 0x0412: r"\CYRV", + 0x0413: r"\CYRG", + 0x0414: r"\CYRD", + 0x0415: r"\CYRE", + 0x0416: r"\CYRZH", + 0x0417: r"\CYRZ", + 0x0418: r"\CYRI", + 0x0419: r"\CYRISHRT", + 0x041A: r"\CYRK", + 0x041B: r"\CYRL", + 0x041C: r"\CYRM", + 0x041D: r"\CYRN", + 0x041E: r"\CYRO", + 0x041F: r"\CYRP", + 0x0420: r"\CYRR", + 0x0421: r"\CYRS", + 0x0422: r"\CYRT", + 0x0423: r"\CYRU", + 0x0424: r"\CYRF", + 0x0425: r"\CYRH", + 0x0426: r"\CYRC", + 0x0427: r"\CYRCH", + 0x0428: r"\CYRSH", + 0x0429: r"\CYRSHCH", + 0x042A: r"\CYRHRDSN", + 0x042B: r"\CYRERY", + 0x042C: r"\CYRSFTSN", + 0x042D: r"\CYREREV", + 0x042E: r"\CYRYU", + 0x042F: r"\CYRYA", + 0x0430: r"\cyra", + 0x0431: r"\cyrb", + 0x0432: r"\cyrv", + 0x0433: r"\cyrg", + 0x0434: r"\cyrd", + 0x0435: r"\cyre", + 0x0436: r"\cyrzh", + 0x0437: r"\cyrz", + 0x0438: r"\cyri", + 0x0439: r"\cyrishrt", + 0x043A: r"\cyrk", + 0x043B: r"\cyrl", + 0x043C: r"\cyrm", + 0x043D: r"\cyrn", + 0x043E: r"\cyro", + 0x043F: r"\cyrp", + 0x0440: r"\cyrr", + 0x0441: r"\cyrs", + 0x0442: r"\cyrt", + 0x0443: r"\cyru", + 0x0444: r"\cyrf", + 0x0445: r"\cyrh", + 0x0446: r"\cyrc", + 0x0447: r"\cyrch", + 0x0448: r"\cyrsh", + 0x0449: r"\cyrshch", + 0x044A: r"\cyrhrdsn", + 0x044B: r"\cyrery", + 0x044C: r"\cyrsftsn", + 0x044D: r"\cyrerev", + 0x044E: r"\cyryu", + 0x044F: r"\cyrya", + 0x0450: r"\`\cyre", + 0x0451: r"\cyryo", + 0x0452: r"\cyrdje", + 0x0453: r"\`\cyrg", + 0x0454: r"\cyrie", + 0x0455: r"\cyrdze", + 0x0456: r"\cyrii", + 0x0457: r"\cyryi", + 0x0458: r"\cyrje", + 0x0459: r"\cyrlje", + 0x045A: r"\cyrnje", + 0x045B: r"\cyrtshe", + 0x045C: r"\`\cyrk", + 0x045D: r"\`\cyri", + 0x045E: r"\cyrushrt", + 0x045F: r"\cyrdzhe", + 0x0460: r"\cyrchar\CYROMEGA", # CYRILLIC CAPITAL LETTER OMEGA [Ѡ] + 0x0461: r"\cyrchar\cyromega", # CYRILLIC SMALL LETTER OMEGA [ѡ] + 0x0462: r"\CYRYAT", + 0x0463: r"\cyryat", + 0x0464: r"\cyrchar\CYRIOTE", # CYRILLIC CAPITAL LETTER IOTIFIED E [Ѥ] + 0x0465: r"\cyrchar\cyriote", # CYRILLIC SMALL LETTER IOTIFIED E [ѥ] + 0x0466: r"\cyrchar\CYRLYUS", # CYRILLIC CAPITAL LETTER LITTLE YUS [Ѧ] + 0x0467: r"\cyrchar\cyrlyus", # CYRILLIC SMALL LETTER LITTLE YUS [ѧ] + 0x0468: r"\cyrchar\CYRIOTLYUS", # CYRILLIC CAPITAL LETTER IOTIFIED LITTLE YUS [Ѩ] + 0x0469: r"\cyrchar\cyriotlyus", # CYRILLIC SMALL LETTER IOTIFIED LITTLE YUS [ѩ] + 0x046A: r"\CYRBYUS", + 0x046B: r"\cyrbyus", + 0x046C: r"\cyrchar\CYRIOTBYUS", # CYRILLIC CAPITAL LETTER IOTIFIED BIG YUS [Ѭ] + 0x046D: r"\cyrchar\cyriotbyus", # CYRILLIC SMALL LETTER IOTIFIED BIG YUS [ѭ] + 0x046E: r"\cyrchar\CYRKSI", # CYRILLIC CAPITAL LETTER KSI [Ѯ] + 0x046F: r"\cyrchar\cyrksi", # CYRILLIC SMALL LETTER KSI [ѯ] + 0x0470: r"\cyrchar\CYRPSI", # CYRILLIC CAPITAL LETTER PSI [Ѱ] + 0x0471: r"\cyrchar\cyrpsi", # CYRILLIC SMALL LETTER PSI [ѱ] + 0x0472: r"\CYRFITA", + 0x0473: r"\cyrfita", + 0x0474: r"\CYRIZH", + 0x0475: r"\cyrizh", + 0x0476: r"\C\CYRIZH", + 0x0477: r"\C\cyrizh", + 0x0478: r"\cyrchar\CYRUK", # CYRILLIC CAPITAL LETTER UK [Ѹ] + 0x0479: r"\cyrchar\cyruk", # CYRILLIC SMALL LETTER UK [ѹ] + 0x047A: r"\cyrchar\CYROMEGARND", # CYRILLIC CAPITAL LETTER ROUND OMEGA [Ѻ] + 0x047B: r"\cyrchar\cyromegarnd", # CYRILLIC SMALL LETTER ROUND OMEGA [ѻ] + 0x047C: r"\cyrchar\CYROMEGATITLO", # CYRILLIC CAPITAL LETTER OMEGA WITH TITLO [Ѽ] + 0x047D: r"\cyrchar\cyromegatitlo", # CYRILLIC SMALL LETTER OMEGA WITH TITLO [ѽ] + 0x047E: r"\cyrchar\CYROT", # CYRILLIC CAPITAL LETTER OT [Ѿ] + 0x047F: r"\cyrchar\cyrot", # CYRILLIC SMALL LETTER OT [ѿ] + 0x0480: r"\cyrchar\CYRKOPPA", # CYRILLIC CAPITAL LETTER KOPPA [Ҁ] + 0x0481: r"\cyrchar\cyrkoppa", # CYRILLIC SMALL LETTER KOPPA [ҁ] + 0x0482: r"\cyrchar\cyrthousands", # CYRILLIC THOUSANDS SIGN [҂] + 0x0488: r"\cyrchar\cyrhundredthousands", # COMBINING CYRILLIC HUNDRED THOUSANDS SIGN [҈] + 0x0489: r"\cyrchar\cyrmillions", # COMBINING CYRILLIC MILLIONS SIGN [҉] + 0x048C: r"\CYRSEMISFTSN", + 0x048D: r"\cyrsemisftsn", + 0x048E: r"\CYRRTICK", + 0x048F: r"\cyrrtick", + 0x0490: r"\CYRGUP", + 0x0491: r"\cyrgup", + 0x0492: r"\CYRGHCRS", + 0x0493: r"\cyrghcrs", + 0x0494: r"\CYRGHK", + 0x0495: r"\cyrghk", + 0x0496: r"\CYRZHDSC", + 0x0497: r"\cyrzhdsc", + 0x0498: r"\CYRZDSC", + 0x0499: r"\cyrzdsc", + 0x049A: r"\CYRKDSC", + 0x049B: r"\cyrkdsc", + 0x049C: r"\CYRKVCRS", + 0x049D: r"\cyrkvcrs", + 0x049E: r"\CYRKHCRS", + 0x049F: r"\cyrkhcrs", + 0x04A0: r"\CYRKBEAK", + 0x04A1: r"\cyrkbeak", + 0x04A2: r"\CYRNDSC", + 0x04A3: r"\cyrndsc", + 0x04A4: r"\CYRNG", + 0x04A5: r"\cyrng", + 0x04A6: r"\CYRPHK", + 0x04A7: r"\cyrphk", + 0x04A8: r"\CYRABHHA", + 0x04A9: r"\cyrabhha", + 0x04AA: r"\CYRSDSC", + 0x04AB: r"\cyrsdsc", + 0x04AC: r"\CYRTDSC", + 0x04AD: r"\cyrtdsc", + 0x04AE: r"\CYRY", + 0x04AF: r"\cyry", + 0x04B0: r"\CYRYHCRS", + 0x04B1: r"\cyryhcrs", + 0x04B2: r"\CYRHDSC", + 0x04B3: r"\cyrhdsc", + 0x04B4: r"\CYRTETSE", + 0x04B5: r"\cyrtetse", + 0x04B6: r"\CYRCHRDSC", + 0x04B7: r"\cyrchrdsc", + 0x04B8: r"\CYRCHVCRS", + 0x04B9: r"\cyrchvcrs", + 0x04BA: r"\CYRSHHA", + 0x04BB: r"\cyrshha", + 0x04BC: r"\CYRABHCH", + 0x04BD: r"\cyrabhch", + 0x04BE: r"\CYRABHCHDSC", + 0x04BF: r"\cyrabhchdsc", + 0x04C0: r"\CYRpalochka", + 0x04C1: r"\U\CYRZH", + 0x04C2: r"\U\cyrzh", + 0x04C3: r"\CYRKHK", + 0x04C4: r"\cyrkhk", + 0x04C5: r"\CYRLDSC", + 0x04C6: r"\cyrldsc", + 0x04C7: r"\CYRNHK", + 0x04C8: r"\cyrnhk", + 0x04CB: r"\CYRCHLDSC", + 0x04CC: r"\cyrchldsc", + 0x04CD: r"\CYRMDSC", + 0x04CE: r"\cyrmdsc", + 0x04D0: r"\U\CYRA", + 0x04D1: r"\U\cyra", + 0x04D2: r"\"\CYRA", + 0x04D3: r"\"\cyra", + 0x04D4: r"\CYRAE", + 0x04D5: r"\cyrae", + 0x04D6: r"\U\CYRE", + 0x04D7: r"\U\cyre", + 0x04D8: r"\CYRSCHWA", + 0x04D9: r"\cyrschwa", + 0x04DA: r"\"\CYRSCHWA", + 0x04DB: r"\"\cyrschwa", + 0x04DC: r"\"\CYRZH", + 0x04DD: r"\"\cyrzh", + 0x04DE: r"\"\CYRZ", + 0x04DF: r"\"\cyrz", + 0x04E0: r"\CYRABHDZE", + 0x04E1: r"\cyrabhdze", + 0x04E2: r"\=\CYRI", + 0x04E3: r"\=\cyri", + 0x04E4: r"\"\CYRI", + 0x04E5: r"\"\cyri", + 0x04E6: r"\"\CYRO", + 0x04E7: r"\"\cyro", + 0x04E8: r"\CYROTLD", + 0x04E9: r"\cyrotld", + 0x04EC: r"\"\CYREREV", + 0x04ED: r"\"\cyrerev", + 0x04EE: r"\=\CYRU", + 0x04EF: r"\=\cyru", + 0x04F0: r"\"\CYRU", + 0x04F1: r"\"\cyru", + 0x04F2: r"\H\CYRU", + 0x04F3: r"\H\cyru", + 0x04F4: r"\"\CYRCH", + 0x04F5: r"\"\cyrch", + 0x04F6: r"\CYRGDSC", + 0x04F7: r"\cyrgdsc", + 0x04F8: r"\"\CYRERY", + 0x04F9: r"\"\cyrery", + 0x04FA: r"\CYRGDSCHCRS", + 0x04FB: r"\cyrgdschcrs", + 0x04FC: r"\CYRHHK", + 0x04FD: r"\cyrhhk", + 0x04FE: r"\CYRHHCRS", + 0x04FF: r"\cyrhhcrs", # 0x04FF + 0x0E3F: r"\textbaht", + # spaces + 0x2000: r"\enskip", # EN QUAD (= EN SPACE U+2002) + 0x2001: r"\quad", # EM QUAD (= EM SPACE U+2003) + 0x2002: r"\enskip", # EN SPACE + 0x2003: r"\quad", # EM SPACE + 0x2004: r"\hspace{0.33em}", # THREE-PER-EM SPACE + 0x2005: r"\hspace{0.25em}", # FOUR-PER-EM SPACE + 0x2006: r"\hspace{0.167em}", # SIX-PER-EM SPACE + 0x2007: r"~", # FIGURE SPACE + 0x2008: r"\;", # PUNCTUATION SPACE + 0x2009: r"\,", # thin space + 0x200A: r"\hspace{1pt}", # supposed to be thinnest typographical space available + 0x200C: r"\textcompwordmark", # ZERO WIDTH NON-JOINER + 0x2010: r"-", # HYPHEN + 0x2011: r"\nobreakdash-", # NON-BREAKING HYPHEN, https://tex.stackexchange.com/a/330437/32188 + 0x2012: r"-", # FIGURE DASH + 0x2013: r"\textendash", # 0x2013 + 0x2014: r"\textemdash", + 0x2015: r"\textemdash", # HORIZONTAL BAR + 0x2016: r"\ensuremath{\Vert}", + 0x2018: r"\textquoteleft", + 0x2019: r"\textquoteright", + 0x201A: r"\quotesinglbase", # 0x201A + 0x201C: r"\textquotedblleft", + 0x201D: r"\textquotedblright", + 0x201E: r"\quotedblbase", + 0x2020: r"\textdagger", + 0x2021: r"\textdaggerdbl", + 0x2022: r"\textbullet", + 0x2024: r".", # ONE DOT LEADER [․] + 0x2025: r"..", # TWO DOT LEADER [‥] + 0x2026: r"\textellipsis", + 0x2030: r"\textperthousand", + 0x2031: r"\textpertenthousand", + 0x2032: "'", # PRIME [′] + 0x2033: "''", # DOUBLE PRIME [″] + 0x2034: "'''", # TRIPLE PRIME [‴] + 0x2035: r"\ensuremath{\backprime}", # REVERSED PRIME [‵] + 0x2039: r"\guilsinglleft", + 0x203A: r"\guilsinglright", + 0x203B: r"\textreferencemark", + 0x203D: r"\textinterrobang", + 0x2044: r"\textfractionsolidus", + 0x204E: r"\textasteriskcentered", + 0x2052: r"\textdiscount", # 0x2052 + 0x2057: "''''", # QUADRUPLE PRIME [⁗] + 0x205F: r"\hspace{0.22em}", # MEDIUM MATHEMATICAL SPACE [ ] + 0x2060: r"\nolinebreak", # WORD JOINER [⁠] + 0x2061: r"", # FUNCTION APPLICATION + 0x20A1: r"\textcolonmonetary", # 0x20A1 + 0x20A4: r"\textlira", + 0x20A6: r"\textnaira", + 0x20A9: r"\textwon", + 0x20AB: r"\textdong", + 0x20AC: r"\texteuro", + 0x20B1: r"\textpeso", # 0x20B1 + # letter-like symbols + 0x2102: r"\ensuremath{\mathbb{C}}", # DOUBLE-STRUCK CAPITAL C + 0x2103: r"\textcelsius", # DEGREE CELSIUS + 0x2109: r"\ensuremath{^\circ}F", # DEGREE FARENHEIT + 0x210A: r"\ensuremath{g}", # SCRIPT SMALL G + 0x210B: r"\ensuremath{\mathscr{H}}", # SCRIPT CAPITAL H + 0x210C: r"\ensuremath{\mathfrak{H}}", # BLACK-LETTER CAPITAL H + 0x210D: r"\ensuremath{\mathbb{H}}", # DOUBLE-STRUCK CAPITAL H + 0x210E: r"\ensuremath{h}", # PLANCK CONSTANT + 0x210F: r"\ensuremath{\hbar}", # h bar, PLANCK CONSTANT OVER TWO PI + 0x2110: r"\ensuremath{\mathscr{I}}", # SCRIPT CAPITAL I + 0x2111: r"\ensuremath{\mathfrak{I}}", # BLACK-LETTER CAPITAL I + 0x2112: r"\ensuremath{\mathscr{L}}", # SCRIPT CAPITAL L + 0x2113: r"\ensuremath{\ell}", # SCRIPT SMALL L + 0x2115: r"\ensuremath{\mathbb{N}}", # DOUBLE-STRUCK CAPITAL N + 0x2116: r"\textnumero", # NUMERO SIGN + 0x2117: r"\textcircledP", # SOUND RECORDING COPYRIGHT + 0x2118: r"\ensuremath{\wp}", # SCRIPT CAPITAL P [℘] + 0x211E: r"\textrecipe", # PRESCRIPTION TAKE + 0x2119: r"\ensuremath{\mathbb{P}}", # DOUBLE-STRUCK CAPITAL P + 0x211A: r"\ensuremath{\mathbb{Q}}", # DOUBLE-STRUCK CAPITAL Q + 0x211B: r"\ensuremath{\mathscr{R}}", # SCRIPT CAPITAL R + 0x211C: r"\ensuremath{\mathfrak{R}}", # BLACK-LETTER CAPITAL R + 0x211D: r"\ensuremath{\mathbb{R}}", # DOUBLE-STRUCK CAPITAL R + 0x2120: r"\textservicemark", # SERVICE MARK + 0x2122: r"\texttrademark", # TRADE MARK SIGN + 0x2124: r"\ensuremath{\mathbb{Z}}", # DOUBLE-STRUCK CAPITAL Z + 0x2126: r"\textohm", # OHM SIGN + 0x2127: r"\textmho", # OHM SIGN + 0x2128: r"\ensuremath{\mathfrak{Z}}", # BLACK-LETTER CAPITAL Z + 0x212A: r"K", # KELVIN SIGN + 0x212B: r"\r{A}", # ANGSTROM SIGN + 0x212C: r"\ensuremath{\mathscr{B}}", # SCRIPT CAPITAL B + 0x212D: r"\ensuremath{\mathfrak{C}}", # BLACK-LETTER CAPITAL C + 0x212E: r"\textestimated", # ESTIMATED SYMBOL + 0x212F: r"\ensuremath{e}", # SCRIPT SMALL E + 0x2130: r"\ensuremath{\mathscr{E}}", # SCRIPT CAPITAL E + 0x2131: r"\ensuremath{\mathscr{F}}", # SCRIPT CAPITAL F + 0x2133: r"\ensuremath{\mathscr{M}}", # SCRIPT CAPITAL M + 0x2134: r"\ensuremath{o}", # SCRIPT SMALL O + 0x2135: r"\ensuremath{\aleph}", # ALEF SYMBOL + 0x2136: r"\ensuremath{\beth}", # BET SYMBOL [ℶ] + 0x2137: r"\ensuremath{\gimel}", # GIMEL SYMBOL [ℷ] + 0x2138: r"\ensuremath{\daleth}", # DALET SYMBOL [ℸ] + 0x2153: r"\textfrac{1}{3}", # VULGAR FRACTION ONE THIRD [⅓] + 0x2154: r"\textfrac{2}{3}", # VULGAR FRACTION TWO THIRDS [⅔] + 0x2155: r"\textfrac{1}{5}", # VULGAR FRACTION ONE FIFTH [⅕] + 0x2156: r"\textfrac{2}{5}", # VULGAR FRACTION TWO FIFTHS [⅖] + 0x2157: r"\textfrac{3}{5}", # VULGAR FRACTION THREE FIFTHS [⅗] + 0x2158: r"\textfrac{4}{5}", # VULGAR FRACTION FOUR FIFTHS [⅘] + 0x2159: r"\textfrac{1}{6}", # VULGAR FRACTION ONE SIXTH [⅙] + 0x215A: r"\textfrac{5}{6}", # VULGAR FRACTION FIVE SIXTHS [⅚] + 0x215B: r"\textfrac{1}{8}", # VULGAR FRACTION ONE EIGHTH [⅛] + 0x215C: r"\textfrac{3}{8}", # VULGAR FRACTION THREE EIGHTHS [⅜] + 0x215D: r"\textfrac{5}{8}", # VULGAR FRACTION FIVE EIGHTHS [⅝] + 0x215E: r"\textfrac{7}{8}", # VULGAR FRACTION SEVEN EIGHTHS [⅞] + 0x2190: r"\textleftarrow", # 0x2190 + 0x2191: r"\textuparrow", + 0x2192: r"\textrightarrow", + 0x2193: r"\textdownarrow", # 0x2193 + 0x2194: r"\ensuremath{\leftrightarrow}", # LEFT RIGHT ARROW [↔] + 0x2195: r"\ensuremath{\updownarrow}", # UP DOWN ARROW [↕] + 0x2196: r"\ensuremath{\nwarrow}", # NORTH WEST ARROW [↖] + 0x2197: r"\ensuremath{\nearrow}", # NORTH EAST ARROW [↗] + 0x2198: r"\ensuremath{\searrow}", # SOUTH EAST ARROW [↘] + 0x2199: r"\ensuremath{\swarrow}", # SOUTH WEST ARROW [↙] + 0x219A: r"\ensuremath{\nleftarrow}", # LEFTWARDS ARROW WITH STROKE [↚] + 0x219B: r"\ensuremath{\nrightarrow}", # RIGHTWARDS ARROW WITH STROKE [↛] + 0x219C: r"\ensuremath{\arrowwaveleft}", # LEFTWARDS WAVE ARROW [↜] + 0x219D: r"\ensuremath{\arrowwaveright}", # RIGHTWARDS WAVE ARROW [↝] + 0x219E: r"\ensuremath{\twoheadleftarrow}", # LEFTWARDS TWO HEADED ARROW [↞] + 0x21A0: r"\ensuremath{\twoheadrightarrow}", # RIGHTWARDS TWO HEADED ARROW [↠] + 0x21A2: r"\ensuremath{\leftarrowtail}", # LEFTWARDS ARROW WITH TAIL [↢] + 0x21A3: r"\ensuremath{\rightarrowtail}", # RIGHTWARDS ARROW WITH TAIL [↣] + 0x21A6: r"\ensuremath{\mapsto}", # RIGHTWARDS ARROW FROM BAR [↦] + 0x21A9: r"\ensuremath{\hookleftarrow}", # LEFTWARDS ARROW WITH HOOK [↩] + 0x21AA: r"\ensuremath{\hookrightarrow}", # RIGHTWARDS ARROW WITH HOOK [↪] + 0x21AB: r"\ensuremath{\looparrowleft}", # LEFTWARDS ARROW WITH LOOP [↫] + 0x21AC: r"\ensuremath{\looparrowright}", # RIGHTWARDS ARROW WITH LOOP [↬] + 0x21AD: r"\ensuremath{\leftrightsquigarrow}", # LEFT RIGHT WAVE ARROW [↭] + 0x21AE: r"\ensuremath{\nleftrightarrow}", # LEFT RIGHT ARROW WITH STROKE [↮] + 0x21B0: r"\ensuremath{\Lsh}", # UPWARDS ARROW WITH TIP LEFTWARDS [↰] + 0x21B1: r"\ensuremath{\Rsh}", # UPWARDS ARROW WITH TIP RIGHTWARDS [↱] + 0x21B6: r"\ensuremath{\curvearrowleft}", # ANTICLOCKWISE TOP SEMICIRCLE ARROW [↶] + 0x21B7: r"\ensuremath{\curvearrowright}", # CLOCKWISE TOP SEMICIRCLE ARROW [↷] + 0x21BA: r"\ensuremath{\circlearrowleft}", # ANTICLOCKWISE OPEN CIRCLE ARROW [↺] + 0x21BB: r"\ensuremath{\circlearrowright}", # CLOCKWISE OPEN CIRCLE ARROW [↻] + 0x21BC: r"\ensuremath{\leftharpoonup}", # LEFTWARDS HARPOON WITH BARB UPWARDS [↼] + 0x21BD: r"\ensuremath{\leftharpoondown}", # LEFTWARDS HARPOON WITH BARB DOWNWARDS [↽] + 0x21BE: r"\ensuremath{\upharpoonright}", # UPWARDS HARPOON WITH BARB RIGHTWARDS [↾] + 0x21BF: r"\ensuremath{\upharpoonleft}", # UPWARDS HARPOON WITH BARB LEFTWARDS [↿] + 0x21C0: r"\ensuremath{\rightharpoonup}", # RIGHTWARDS HARPOON WITH BARB UPWARDS [⇀] + 0x21C1: r"\ensuremath{\rightharpoondown}", # RIGHTWARDS HARPOON WITH BARB DOWNWARDS [⇁] + 0x21C2: r"\ensuremath{\downharpoonright}", # DOWNWARDS HARPOON WITH BARB RIGHTWARDS [⇂] + 0x21C3: r"\ensuremath{\downharpoonleft}", # DOWNWARDS HARPOON WITH BARB LEFTWARDS [⇃] + 0x21C4: r"\ensuremath{\rightleftarrows}", # RIGHTWARDS ARROW OVER LEFTWARDS ARROW [⇄] + 0x21C5: r"\ensuremath{\dblarrowupdown}", # UPWARDS ARROW LEFTWARDS OF DOWNWARDS ARROW [⇅] + 0x21C6: r"\ensuremath{\leftrightarrows}", # LEFTWARDS ARROW OVER RIGHTWARDS ARROW [⇆] + 0x21C7: r"\ensuremath{\leftleftarrows}", # LEFTWARDS PAIRED ARROWS [⇇] + 0x21C8: r"\ensuremath{\upuparrows}", # UPWARDS PAIRED ARROWS [⇈] + 0x21C9: r"\ensuremath{\rightrightarrows}", # RIGHTWARDS PAIRED ARROWS [⇉] + 0x21CA: r"\ensuremath{\downdownarrows}", # DOWNWARDS PAIRED ARROWS [⇊] + 0x21CB: r"\ensuremath{\leftrightharpoons}", # LEFTWARDS HARPOON OVER RIGHTWARDS HARPOON [⇋] + 0x21CC: r"\ensuremath{\rightleftharpoons}", # RIGHTWARDS HARPOON OVER LEFTWARDS HARPOON [⇌] + 0x21CD: r"\ensuremath{\nLeftarrow}", # LEFTWARDS DOUBLE ARROW WITH STROKE [⇍] + 0x21CE: r"\ensuremath{\nLeftrightarrow}", # LEFT RIGHT DOUBLE ARROW WITH STROKE [⇎] + 0x21CF: r"\ensuremath{\nRightarrow}", # RIGHTWARDS DOUBLE ARROW WITH STROKE [⇏] + 0x21D0: r"\ensuremath{\Leftarrow}", # LEFTWARDS DOUBLE ARROW [⇐] + 0x21D1: r"\ensuremath{\Uparrow}", # UPWARDS DOUBLE ARROW [⇑] + 0x21D2: r"\ensuremath{\Rightarrow}", # RIGHTWARDS DOUBLE ARROW [⇒] + 0x21D3: r"\ensuremath{\Downarrow}", # DOWNWARDS DOUBLE ARROW [⇓] + 0x21D4: r"\ensuremath{\Leftrightarrow}", # LEFT RIGHT DOUBLE ARROW [⇔] + 0x21D5: r"\ensuremath{\Updownarrow}", # UP DOWN DOUBLE ARROW [⇕] + 0x21DA: r"\ensuremath{\Lleftarrow}", # LEFTWARDS TRIPLE ARROW [⇚] + 0x21DB: r"\ensuremath{\Rrightarrow}", # RIGHTWARDS TRIPLE ARROW [⇛] + 0x21DD: r"\ensuremath{\rightsquigarrow}", # RIGHTWARDS SQUIGGLE ARROW [⇝] + 0x21F5: r"\ensuremath{\DownArrowUpArrow}", # DOWNWARDS ARROW LEFTWARDS OF UPWARDS ARROW [⇵] + # Math operators and symbols (U+22XX) + 0x2200: r"\ensuremath{\forall}", + 0x2201: r"\ensuremath{\complement}", + 0x2202: r"\ensuremath{\partial}", + 0x2203: r"\ensuremath{\exists}", + 0x2204: r"\ensuremath{\nexists}", + 0x2205: r"\ensuremath{\varnothing}", + 0x2206: r"\ensuremath{\Delta}", + 0x2207: r"\ensuremath{\nabla}", + 0x2208: r"\ensuremath{\in}", + 0x2209: r"\ensuremath{\notin}", + 0x220A: r"\ensuremath{\in}", # alternative + 0x220B: r"\ensuremath{\ni}", + 0x220C: r"\ensuremath{\not\ni}", + 0x220D: r"\ensuremath{\ni}", # alternative + 0x220E: r"\ensuremath{\blacksquare}", + 0x220F: r"\ensuremath{\prod}", + 0x2210: r"\ensuremath{\coprod}", + 0x2211: r"\ensuremath{\sum}", + 0x2212: r"\ensuremath{-}", + 0x2213: r"\ensuremath{\mp}", + 0x2214: r"\ensuremath{\dotplus}", # DOT PLUS [∔] + 0x2215: r"\ensuremath{/}", + 0x2216: r"\ensuremath{\smallsetminus}", + 0x2217: r"\ensuremath{*}", + 0x2218: r"\ensuremath{\circ}", + 0x2219: r"\ensuremath{\bullet}", + 0x221A: r"\ensuremath{\sqrt{}}", + 0x221B: r"\ensuremath{\sqrt[3]{}}", + 0x221C: r"\ensuremath{\sqrt[4]{}}", + 0x221D: r"\ensuremath{\propto}", + 0x221E: r"\ensuremath{\infty}", + 0x221F: r"\ensuremath{\rightangle}", # RIGHT ANGLE [∟] + 0x2220: r"\ensuremath{\angle}", # ANGLE [∠] + 0x2221: r"\ensuremath{\measuredangle}", # MEASURED ANGLE [∡] + 0x2222: r"\ensuremath{\sphericalangle}", # SPHERICAL ANGLE [∢] + 0x2223: r"\ensuremath{\mid}", + 0x2224: r"\ensuremath{\nmid}", + 0x2225: r"\ensuremath{\parallel}", + 0x2226: r"\ensuremath{\nparallel}", + 0x2227: r"\ensuremath{\wedge}", + 0x2228: r"\ensuremath{\vee}", + 0x2229: r"\ensuremath{\cap}", + 0x222A: r"\ensuremath{\cup}", + 0x222B: r"\ensuremath{\int}", + 0x222C: r"\ensuremath{\iint}", + 0x222D: r"\ensuremath{\iiint}", + 0x222E: r"\ensuremath{\oint}", + 0x222F: r"\ensuremath{\surfintegral}", # SURFACE INTEGRAL [∯] + 0x2230: r"\ensuremath{\volintegral}", # VOLUME INTEGRAL [∰] + 0x2231: r"\ensuremath{\clwintegral}", # CLOCKWISE INTEGRAL [∱] + # 0x2232: CLOCKWISE CONTOUR INTEGRAL + # 0x2233: ANTICLOCKWISE CONTOUR INTEGRAL + 0x2234: r"\ensuremath{\therefore}", + 0x2235: r"\ensuremath{\because}", + 0x2236: r"\ensuremath{:}", + 0x2237: r"\ensuremath{::}", + # 0x2238: DOT MINUS + # ... + 0x223A: r"\ensuremath{\mathbin{{:}\!\!{-}\!\!{:}}}", # GEOMETRIC PROPORTION [∺] + 0x223B: r"\ensuremath{\homothetic}", # HOMOTHETIC [∻] + 0x223C: r"\ensuremath{\sim}", + 0x223D: r"\ensuremath{\backsim}", + 0x223E: r"\ensuremath{\lazysinv}", # INVERTED LAZY S [∾] + # + 0x2240: r"\ensuremath{\wr}", # WREATH PRODUCT [≀] + 0x2241: r"\ensuremath{\not\sim}", # NOT TILDE [≁] + 0x2243: r"\ensuremath{\simeq}", # ASYMPTOTICALLY EQUAL TO [≃] + 0x2244: r"\ensuremath{\not\simeq}", # NOT ASYMPTOTICALLY EQUAL TO [≄] + 0x2245: r"\ensuremath{\cong}", # APPROXIMATELY EQUAL TO [≅] + 0x2246: r"\ensuremath{\approxnotequal}", # APPROXIMATELY BUT NOT ACTUALLY EQUAL TO [≆] + 0x2247: r"\ensuremath{\not\cong}", # NEITHER APPROXIMATELY NOR ACTUALLY EQUAL TO [≇] + 0x2248: r"\ensuremath{\approx}", + 0x2249: r"\ensuremath{\not\approx}", # NOT ALMOST EQUAL TO [≉] + 0x224A: r"\ensuremath{\approxeq}", # ALMOST EQUAL OR EQUAL TO [≊] + 0x224B: r"\ensuremath{\tildetrpl}", # TRIPLE TILDE [≋] + 0x224C: r"\ensuremath{\allequal}", # ALL EQUAL TO [≌] + 0x224D: r"\ensuremath{\asymp}", # EQUIVALENT TO [≍] + 0x224E: r"\ensuremath{\Bumpeq}", # GEOMETRICALLY EQUIVALENT TO [≎] + 0x224F: r"\ensuremath{\bumpeq}", # DIFFERENCE BETWEEN [≏] + 0x2250: r"\ensuremath{\doteq}", # APPROACHES THE LIMIT [≐] + 0x2251: r"\ensuremath{\doteqdot}", # GEOMETRICALLY EQUAL TO [≑] + 0x2252: r"\ensuremath{\fallingdotseq}", # APPROXIMATELY EQUAL TO OR THE IMAGE OF [≒] + 0x2253: r"\ensuremath{\risingdotseq}", # IMAGE OF OR APPROXIMATELY EQUAL TO [≓] + 0x2254: r"\ensuremath{:=}", # COLON EQUALS [≔] + 0x2255: r"\ensuremath{=:}", # EQUALS COLON [≕] + 0x2256: r"\ensuremath{\eqcirc}", # RING IN EQUAL TO [≖] + 0x2257: r"\ensuremath{\circeq}", # RING EQUAL TO [≗] + 0x2259: r"\ensuremath{\estimates}", # ESTIMATES [≙] + 0x225B: r"\ensuremath{\starequal}", # STAR EQUALS [≛] + 0x225C: r"\ensuremath{\triangleq}", # DELTA EQUAL TO [≜] + # + 0x2260: r"\ensuremath{\neq}", + 0x2261: r"\ensuremath{\equiv}", + 0x2262: r"\ensuremath{\not\equiv}", + # 0x2263: STRICTLY EQUIVALENT TO + 0x2264: r"\ensuremath{\leq}", + 0x2265: r"\ensuremath{\geq}", + 0x2266: r"\ensuremath{\leqq}", + 0x2267: r"\ensuremath{\geqq}", + 0x2268: r"\ensuremath{\lneqq}", + 0x2269: r"\ensuremath{\gneqq}", + 0x226A: r"\ensuremath{\ll}", + 0x226B: r"\ensuremath{\gg}", + 0x226C: r"\ensuremath{\between}", # BETWEEN [≬] + 0x226D: r"\ensuremath{\not\kern-0.3em\times}", # NOT EQUIVALENT TO [≭] + 0x226E: r"\ensuremath{\nless}", + 0x226F: r"\ensuremath{\ngtr}", + 0x2270: r"\ensuremath{\nleq}", + 0x2271: r"\ensuremath{\ngeq}", + 0x2272: r"\ensuremath{\lesssim}", + 0x2273: r"\ensuremath{\gtrsim}", + 0x2274: r"\ensuremath{\not\lesssim}", + 0x2275: r"\ensuremath{\not\gtrsim}", + 0x2276: r"\ensuremath{\lessgtr}", + 0x2277: r"\ensuremath{\gtrless}", + 0x2278: r"\ensuremath{\notlessgreater}", # NEITHER LESS-THAN NOR GREATER-THAN [≸] + 0x2279: r"\ensuremath{\notgreaterless}", # NEITHER GREATER-THAN NOR LESS-THAN [≹] + 0x227A: r"\ensuremath{\prec}", + 0x227B: r"\ensuremath{\succ}", + 0x227C: r"\ensuremath{\preceq}", + 0x227D: r"\ensuremath{\succeq}", + 0x227E: r"\ensuremath{\precsim}", + 0x227F: r"\ensuremath{\succsim}", + 0x2280: r"\ensuremath{\nprec}", + 0x2281: r"\ensuremath{\nsucc}", + 0x2282: r"\ensuremath{\subset}", + 0x2283: r"\ensuremath{\supset}", + 0x2284: r"\ensuremath{\not\subset}", + 0x2285: r"\ensuremath{\not\supset}", + 0x2286: r"\ensuremath{\subseteq}", + 0x2287: r"\ensuremath{\supseteq}", + 0x2288: r"\ensuremath{\nsubseteq}", + 0x2289: r"\ensuremath{\nsupseteq}", + 0x228A: r"\ensuremath{\subsetneq}", + 0x228B: r"\ensuremath{\supsetneq}", + 0x228E: r"\ensuremath{\uplus}", # MULTISET UNION [⊎] + 0x228F: r"\ensuremath{\sqsubset}", # SQUARE IMAGE OF [⊏] + 0x2290: r"\ensuremath{\sqsupset}", # SQUARE ORIGINAL OF [⊐] + 0x2291: r"\ensuremath{\sqsubseteq}", # SQUARE IMAGE OF OR EQUAL TO [⊑] + 0x2292: r"\ensuremath{\sqsupseteq}", # SQUARE ORIGINAL OF OR EQUAL TO [⊒] + 0x2293: r"\ensuremath{\sqcap}", + 0x2294: r"\ensuremath{\sqcup}", + 0x2295: r"\ensuremath{\oplus}", + 0x2296: r"\ensuremath{\ominus}", + 0x2297: r"\ensuremath{\otimes}", + 0x2298: r"\ensuremath{\oslash}", + 0x2299: r"\ensuremath{\odot}", + 0x229A: r"\ensuremath{\circledcirc}", # CIRCLED RING OPERATOR [⊚] + 0x229B: r"\ensuremath{\circledast}", # CIRCLED ASTERISK OPERATOR [⊛] + 0x229D: r"\ensuremath{\circleddash}", # CIRCLED DASH [⊝] + 0x229E: r"\ensuremath{\boxplus}", # SQUARED PLUS [⊞] + 0x229F: r"\ensuremath{\boxminus}", # SQUARED MINUS [⊟] + 0x22A0: r"\ensuremath{\boxtimes}", # SQUARED TIMES [⊠] + 0x22A1: r"\ensuremath{\boxdot}", # SQUARED DOT OPERATOR [⊡] + 0x22A2: r"\ensuremath{\vdash}", # RIGHT TACK [⊢] + 0x22A3: r"\ensuremath{\dashv}", # LEFT TACK [⊣] + 0x22A4: r"\ensuremath{\top}", # DOWN TACK [⊤] + 0x22A5: r"\ensuremath{\perp}", # UP TACK [⊥] + 0x22A7: r"\ensuremath{\truestate}", # MODELS [⊧] + 0x22A8: r"\ensuremath{\forcesextra}", # TRUE [⊨] + 0x22A9: r"\ensuremath{\Vdash}", # FORCES [⊩] + 0x22AA: r"\ensuremath{\Vvdash}", # TRIPLE VERTICAL BAR RIGHT TURNSTILE [⊪] + 0x22AB: r"\ensuremath{\VDash}", # DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE [⊫] + 0x22AC: r"\ensuremath{\nvdash}", # DOES NOT PROVE [⊬] + 0x22AD: r"\ensuremath{\nvDash}", # NOT TRUE [⊭] + 0x22AE: r"\ensuremath{\nVdash}", # DOES NOT FORCE [⊮] + 0x22AF: r"\ensuremath{\nVDash}", # NEGATED DOUBLE VERTICAL BAR DOUBLE RIGHT TURNSTILE [⊯] + 0x22B2: r"\ensuremath{\vartriangleleft}", # NORMAL SUBGROUP OF [⊲] + 0x22B3: r"\ensuremath{\vartriangleright}", # CONTAINS AS NORMAL SUBGROUP [⊳] + 0x22B4: r"\ensuremath{\trianglelefteq}", # NORMAL SUBGROUP OF OR EQUAL TO [⊴] + 0x22B5: r"\ensuremath{\trianglerighteq}", # CONTAINS AS NORMAL SUBGROUP OR EQUAL TO [⊵] + 0x22B6: r"\ensuremath{\original}", # ORIGINAL OF [⊶] + 0x22B7: r"\ensuremath{\image}", # IMAGE OF [⊷] + 0x22B8: r"\ensuremath{\multimap}", # MULTIMAP [⊸] + 0x22B9: r"\ensuremath{\hermitconjmatrix}", # HERMITIAN CONJUGATE MATRIX [⊹] + 0x22BA: r"\ensuremath{\intercal}", # INTERCALATE [⊺] + 0x22BB: r"\ensuremath{\veebar}", # XOR [⊻] + 0x22BE: r"\ensuremath{\rightanglearc}", # RIGHT ANGLE WITH ARC [⊾] + 0x22C0: r"\ensuremath{\bigwedge}", + 0x22C1: r"\ensuremath{\bigvee}", + 0x22C2: r"\ensuremath{\bigcap}", + 0x22C3: r"\ensuremath{\bigcup}", + 0x22C4: r"\ensuremath{\diamond}", + 0x22C5: r"\ensuremath{\cdot}", + 0x22C6: r"\ensuremath{\star}", + 0x22C7: r"\ensuremath{\divideontimes}", + 0x22C8: r"\ensuremath{\bowtie}", + 0x22C9: r"\ensuremath{\ltimes}", + 0x22CA: r"\ensuremath{\rtimes}", + 0x22CB: r"\ensuremath{\leftthreetimes}", + 0x22CC: r"\ensuremath{\rightthreetimes}", + 0x22CD: r"\ensuremath{\backsimeq}", # REVERSED TILDE EQUALS [⋍] + 0x22CE: r"\ensuremath{\curlyvee}", # CURLY LOGICAL OR [⋎] + 0x22CF: r"\ensuremath{\curlywedge}", # CURLY LOGICAL AND [⋏] + 0x22D0: r"\ensuremath{\Subset}", # DOUBLE SUBSET [⋐] + 0x22D1: r"\ensuremath{\Supset}", # DOUBLE SUPERSET [⋑] + 0x22D2: r"\ensuremath{\Cap}", # DOUBLE INTERSECTION [⋒] + 0x22D3: r"\ensuremath{\Cup}", # DOUBLE UNION [⋓] + 0x22D4: r"\ensuremath{\pitchfork}", # PITCHFORK [⋔] + 0x22D6: r"\ensuremath{\lessdot}", # LESS-THAN WITH DOT [⋖] + 0x22D7: r"\ensuremath{\gtrdot}", # GREATER-THAN WITH DOT [⋗] + 0x22D8: r"\ensuremath{\verymuchless}", # VERY MUCH LESS-THAN [⋘] + 0x22D9: r"\ensuremath{\verymuchgreater}", # VERY MUCH GREATER-THAN [⋙] + 0x22DA: r"\ensuremath{\lesseqgtr}", # LESS-THAN EQUAL TO OR GREATER-THAN [⋚] + 0x22DB: r"\ensuremath{\gtreqless}", # GREATER-THAN EQUAL TO OR LESS-THAN [⋛] + 0x22DE: r"\ensuremath{\curlyeqprec}", # EQUAL TO OR PRECEDES [⋞] + 0x22DF: r"\ensuremath{\curlyeqsucc}", # EQUAL TO OR SUCCEEDS [⋟] + 0x22E2: r"\ensuremath{\not\sqsubseteq}", # NOT SQUARE IMAGE OF OR EQUAL TO [⋢] + 0x22E3: r"\ensuremath{\not\sqsupseteq}", # NOT SQUARE ORIGINAL OF OR EQUAL TO [⋣] + 0x22E6: r"\ensuremath{\lnsim}", # LESS-THAN BUT NOT EQUIVALENT TO [⋦] + 0x22E7: r"\ensuremath{\gnsim}", # GREATER-THAN BUT NOT EQUIVALENT TO [⋧] + 0x22E8: r"\ensuremath{\precedesnotsimilar}", # PRECEDES BUT NOT EQUIVALENT TO [⋨] + 0x22E9: r"\ensuremath{\succnsim}", # SUCCEEDS BUT NOT EQUIVALENT TO [⋩] + 0x22EA: r"\ensuremath{\ntriangleleft}", # NOT NORMAL SUBGROUP OF [⋪] + 0x22EB: r"\ensuremath{\ntriangleright}", # DOES NOT CONTAIN AS NORMAL SUBGROUP [⋫] + 0x22EC: r"\ensuremath{\ntrianglelefteq}", # NOT NORMAL SUBGROUP OF OR EQUAL TO [⋬] + 0x22ED: r"\ensuremath{\ntrianglerighteq}", # DOES NOT CONTAIN AS NORMAL SUBGROUP OR EQUAL [⋭] + 0x22EE: r"\ensuremath{\vdots}", + 0x22EF: r"\ensuremath{\cdots}", + 0x22F0: r"\ensuremath{\udots}", + 0x22F1: r"\ensuremath{\ddots}", + # ... + 0x2305: r"\ensuremath{\barwedge}", # PROJECTIVE [⌅] + 0x2306: r"\ensuremath{\varperspcorrespond}", # PERSPECTIVE [⌆] + 0x2308: r"\ensuremath{\lceil}", # LEFT CEILING [⌈] + 0x2309: r"\ensuremath{\rceil}", # RIGHT CEILING [⌉] + 0x230A: r"\ensuremath{\lfloor}", # LEFT FLOOR [⌊] + 0x230B: r"\ensuremath{\rfloor}", # RIGHT FLOOR [⌋] + 0x2315: r"\ensuremath{\recorder}", # TELEPHONE RECORDER [⌕] + 0x2316: r'\ensuremath{\mathchar"2208}', # POSITION INDICATOR [⌖] + 0x231C: r"\ensuremath{\ulcorner}", # TOP LEFT CORNER [⌜] + 0x231D: r"\ensuremath{\urcorner}", # TOP RIGHT CORNER [⌝] + 0x231E: r"\ensuremath{\llcorner}", # BOTTOM LEFT CORNER [⌞] + 0x231F: r"\ensuremath{\lrcorner}", # BOTTOM RIGHT CORNER [⌟] + 0x2322: r"\ensuremath{\frown}", # FROWN [⌢] + 0x2323: r"\ensuremath{\smile}", # SMILE [⌣] + 0x23B0: r"\ensuremath{\lmoustache}", # UPPER LEFT OR LOWER RIGHT CURLY BRACKET SECTION [⎰] + 0x23B1: r"\ensuremath{\rmoustache}", # UPPER RIGHT OR LOWER LEFT CURLY BRACKET SECTION [⎱] + 0x2329: r"\textlangle", # 0x2329 + 0x232A: r"\textrangle", + 0x2422: r"\textblank", + 0x2423: r"\textvisiblespace", + 0x25A0: r"\ensuremath{\blacksquare}", # BLACK SQUARE [■] + 0x25A1: r"\ensuremath{\square}", # WHITE SQUARE [□] + 0x25AA: r"{\small\ensuremath{\blacksquare}}", # BLACK SMALL SQUARE [▪] + 0x25AD: r"\fbox{~~}", # WHITE RECTANGLE [▭] + 0x25B3: r"\ensuremath{\bigtriangleup}", # WHITE UP-POINTING TRIANGLE [△] + 0x25B4: r"\ensuremath{\blacktriangle}", # BLACK UP-POINTING SMALL TRIANGLE [▴] + 0x25B5: r"\ensuremath{\vartriangle}", # WHITE UP-POINTING SMALL TRIANGLE [▵] + 0x25B8: r"\ensuremath{\blacktriangleright}", # BLACK RIGHT-POINTING SMALL TRIANGLE [▸] + 0x25B9: r"\ensuremath{\triangleright}", # WHITE RIGHT-POINTING SMALL TRIANGLE [▹] + 0x25BD: r"\ensuremath{\bigtriangledown}", # WHITE DOWN-POINTING TRIANGLE [▽] + 0x25BE: r"\ensuremath{\blacktriangledown}", # BLACK DOWN-POINTING SMALL TRIANGLE [▾] + 0x25BF: r"\ensuremath{\triangledown}", # WHITE DOWN-POINTING SMALL TRIANGLE [▿] + 0x25C2: r"\ensuremath{\blacktriangleleft}", # BLACK LEFT-POINTING SMALL TRIANGLE [◂] + 0x25C3: r"\ensuremath{\triangleleft}", # WHITE LEFT-POINTING SMALL TRIANGLE [◃] + 0x25CA: r"\ensuremath{\lozenge}", # LOZENGE [◊] + 0x25CB: r"\ensuremath{\bigcirc}", # WHITE CIRCLE [○] + 0x25E6: r"\textopenbullet", + 0x25EF: r"\textbigcircle", + 0x2662: r"\ensuremath{\diamond}", # WHITE DIAMOND SUIT [♢] + 0x266A: r"\textmusicalnote", # 0x266A + 0x2669: r"\quarternote", # QUARTER NOTE [♩] + 0x266D: r"\flat", # MUSIC FLAT SIGN [♭] + 0x266E: r"\natural", # MUSIC NATURAL SIGN [♮] + 0x266F: r"\sharp", # MUSIC SHARP SIGN [♯] + 0x27E8: r"\ensuremath{\langle}", # MATHEMATICAL LEFT ANGLE BRACKET + 0x27E9: r"\ensuremath{\rangle}", # MATHEMATICAL RIGHT ANGLE BRACKET + 0x27F5: r"\ensuremath{\longleftarrow}", # LONG LEFTWARDS ARROW [⟵] + 0x27F6: r"\ensuremath{\longrightarrow}", # LONG RIGHTWARDS ARROW [⟶] + 0x27F7: r"\ensuremath{\longleftrightarrow}", # LONG LEFT RIGHT ARROW [⟷] + 0x27F8: r"\ensuremath{\Longleftarrow}", # LONG LEFTWARDS DOUBLE ARROW [⟸] + 0x27F9: r"\ensuremath{\Longrightarrow}", # LONG RIGHTWARDS DOUBLE ARROW [⟹] + 0x27FA: r"\ensuremath{\Longleftrightarrow}", # LONG LEFT RIGHT DOUBLE ARROW [⟺] + 0x27FC: r"\ensuremath{\longmapsto}", # LONG RIGHTWARDS ARROW FROM BAR [⟼] + 0x27FF: r"\ensuremath{\sim\joinrel\leadsto}", # LONG RIGHTWARDS SQUIGGLE ARROW [⟿] + 0x2993: r"\ensuremath{<\kern-0.58em(}", # LEFT ARC LESS-THAN BRACKET [⦓] + 0x29EB: r"\ensuremath{\blacklozenge}", # BLACK LOZENGE [⧫] + # Supplemental Mathematical Operators U+2AXX + 0x2A0F: r"\ensuremath{\clockoint}", # INTEGRAL AVERAGE WITH SLASH [⨏] + 0x2A16: r"\ensuremath{\sqrint}", # QUATERNION INTEGRAL OPERATOR [⨖] + 0x2A3F: r"\ensuremath{\amalg}", # AMALGAMATION OR COPRODUCT [⨿] + 0x2A6E: r"\ensuremath{\stackrel{*}{=}}", # EQUALS WITH ASTERISK [⩮] + 0x2A75: r"==", # TWO CONSECUTIVE EQUALS SIGNS [⩵] + 0x2A7D: r"\ensuremath{\leqslant}", + 0x2A7E: r"\ensuremath{\geqslant}", + 0x2A85: r"\ensuremath{\lessapprox}", # LESS-THAN OR APPROXIMATE [⪅] + 0x2A86: r"\ensuremath{\gtrapprox}", # GREATER-THAN OR APPROXIMATE [⪆] + 0x2A87: r"\ensuremath{\lneq}", # LESS-THAN AND SINGLE-LINE NOT EQUAL TO [⪇] + 0x2A88: r"\ensuremath{\gneq}", # GREATER-THAN AND SINGLE-LINE NOT EQUAL TO [⪈] + 0x2A89: r"\ensuremath{\lnapprox}", # LESS-THAN AND NOT APPROXIMATE [⪉] + 0x2A8A: r"\ensuremath{\gnapprox}", # GREATER-THAN AND NOT APPROXIMATE [⪊] + 0x2A8B: r"\ensuremath{\lesseqqgtr}", # LESS-THAN ABOVE DOUBLE-LINE EQUAL ABOVE GREATER-THAN [⪋] + 0x2A8C: r"\ensuremath{\gtreqqless}", # GREATER-THAN ABOVE DOUBLE-LINE EQUAL ABOVE LESS-THAN [⪌] + 0x2A95: r"\ensuremath{\eqslantless}", # SLANTED EQUAL TO OR LESS-THAN [⪕] + 0x2A96: r"\ensuremath{\eqslantgtr}", # SLANTED EQUAL TO OR GREATER-THAN [⪖] + 0x2AAF: r"\ensuremath{\preceq}", # PRECEDES ABOVE SINGLE-LINE EQUALS SIGN [⪯] + 0x2AB0: r"\ensuremath{\succeq}", # SUCCEEDS ABOVE SINGLE-LINE EQUALS SIGN [⪰] + 0x2AB5: r"\ensuremath{\precneqq}", # PRECEDES ABOVE NOT EQUAL TO [⪵] + 0x2AB6: r"\ensuremath{\succneqq}", # SUCCEEDS ABOVE NOT EQUAL TO [⪶] + 0x2AB7: r"\ensuremath{\precapprox}", # PRECEDES ABOVE ALMOST EQUAL TO [⪷] + 0x2AB8: r"\ensuremath{\succapprox}", # SUCCEEDS ABOVE ALMOST EQUAL TO [⪸] + 0x2AB9: r"\ensuremath{\precnapprox}", # PRECEDES ABOVE NOT ALMOST EQUAL TO [⪹] + 0x2ABA: r"\ensuremath{\succnapprox}", # SUCCEEDS ABOVE NOT ALMOST EQUAL TO [⪺] + 0x2AC5: r"\ensuremath{\subseteqq}", # SUBSET OF ABOVE EQUALS SIGN [⫅] + 0x2AC6: r"\ensuremath{\supseteqq}", # SUPERSET OF ABOVE EQUALS SIGN [⫆] + 0x2ACB: r"\ensuremath{\subsetneqq}", # SUBSET OF ABOVE NOT EQUAL TO [⫋] + 0x2ACC: r"\ensuremath{\supsetneqq}", # SUPERSET OF ABOVE NOT EQUAL TO [⫌] + 0x2AFD: r"\ensuremath{{{/}\!\!{/}}}", # DOUBLE SOLIDUS OPERATOR [⫽] + # CJK Symbols Punktuation (!) U+3000 : for \langle/\rangle + 0x3008: r"\ensuremath{\langle}", + 0x3009: r"\ensuremath{\rangle}", + # ligatures + 0xFB00: r"ff", # LATIN SMALL LIGATURE FF + 0xFB01: r"fi", # LATIN SMALL LIGATURE FI + 0xFB02: r"fl", # LATIN SMALL LIGATURE FL + 0xFB03: r"ffi", # LATIN SMALL LIGATURE FFI + 0xFB04: r"ffl", # LATIN SMALL LIGATURE FFL + # Mathematical Alphanumeric Symbols + 0x1D400: r"\ensuremath{\mathbf{A}}", # MATHEMATICAL BOLD CAPITAL A + 0x1D401: r"\ensuremath{\mathbf{B}}", # MATHEMATICAL BOLD CAPITAL B + 0x1D402: r"\ensuremath{\mathbf{C}}", # MATHEMATICAL BOLD CAPITAL C + 0x1D403: r"\ensuremath{\mathbf{D}}", # MATHEMATICAL BOLD CAPITAL D + 0x1D404: r"\ensuremath{\mathbf{E}}", # MATHEMATICAL BOLD CAPITAL E + 0x1D405: r"\ensuremath{\mathbf{F}}", # MATHEMATICAL BOLD CAPITAL F + 0x1D406: r"\ensuremath{\mathbf{G}}", # MATHEMATICAL BOLD CAPITAL G + 0x1D407: r"\ensuremath{\mathbf{H}}", # MATHEMATICAL BOLD CAPITAL H + 0x1D408: r"\ensuremath{\mathbf{I}}", # MATHEMATICAL BOLD CAPITAL I + 0x1D409: r"\ensuremath{\mathbf{J}}", # MATHEMATICAL BOLD CAPITAL J + 0x1D40A: r"\ensuremath{\mathbf{K}}", # MATHEMATICAL BOLD CAPITAL K + 0x1D40B: r"\ensuremath{\mathbf{L}}", # MATHEMATICAL BOLD CAPITAL L + 0x1D40C: r"\ensuremath{\mathbf{M}}", # MATHEMATICAL BOLD CAPITAL M + 0x1D40D: r"\ensuremath{\mathbf{N}}", # MATHEMATICAL BOLD CAPITAL N + 0x1D40E: r"\ensuremath{\mathbf{O}}", # MATHEMATICAL BOLD CAPITAL O + 0x1D40F: r"\ensuremath{\mathbf{P}}", # MATHEMATICAL BOLD CAPITAL P + 0x1D410: r"\ensuremath{\mathbf{Q}}", # MATHEMATICAL BOLD CAPITAL Q + 0x1D411: r"\ensuremath{\mathbf{R}}", # MATHEMATICAL BOLD CAPITAL R + 0x1D412: r"\ensuremath{\mathbf{S}}", # MATHEMATICAL BOLD CAPITAL S + 0x1D413: r"\ensuremath{\mathbf{T}}", # MATHEMATICAL BOLD CAPITAL T + 0x1D414: r"\ensuremath{\mathbf{U}}", # MATHEMATICAL BOLD CAPITAL U + 0x1D415: r"\ensuremath{\mathbf{V}}", # MATHEMATICAL BOLD CAPITAL V + 0x1D416: r"\ensuremath{\mathbf{W}}", # MATHEMATICAL BOLD CAPITAL W + 0x1D417: r"\ensuremath{\mathbf{X}}", # MATHEMATICAL BOLD CAPITAL X + 0x1D418: r"\ensuremath{\mathbf{Y}}", # MATHEMATICAL BOLD CAPITAL Y + 0x1D419: r"\ensuremath{\mathbf{Z}}", # MATHEMATICAL BOLD CAPITAL Z + 0x1D41A: r"\ensuremath{\mathbf{a}}", # MATHEMATICAL BOLD SMALL a + 0x1D41B: r"\ensuremath{\mathbf{b}}", # MATHEMATICAL BOLD SMALL b + 0x1D41C: r"\ensuremath{\mathbf{c}}", # MATHEMATICAL BOLD SMALL c + 0x1D41D: r"\ensuremath{\mathbf{d}}", # MATHEMATICAL BOLD SMALL d + 0x1D41E: r"\ensuremath{\mathbf{e}}", # MATHEMATICAL BOLD SMALL e + 0x1D41F: r"\ensuremath{\mathbf{f}}", # MATHEMATICAL BOLD SMALL f + 0x1D420: r"\ensuremath{\mathbf{g}}", # MATHEMATICAL BOLD SMALL g + 0x1D421: r"\ensuremath{\mathbf{h}}", # MATHEMATICAL BOLD SMALL h + 0x1D422: r"\ensuremath{\mathbf{i}}", # MATHEMATICAL BOLD SMALL i + 0x1D423: r"\ensuremath{\mathbf{j}}", # MATHEMATICAL BOLD SMALL j + 0x1D424: r"\ensuremath{\mathbf{k}}", # MATHEMATICAL BOLD SMALL k + 0x1D425: r"\ensuremath{\mathbf{l}}", # MATHEMATICAL BOLD SMALL l + 0x1D426: r"\ensuremath{\mathbf{m}}", # MATHEMATICAL BOLD SMALL m + 0x1D427: r"\ensuremath{\mathbf{n}}", # MATHEMATICAL BOLD SMALL n + 0x1D428: r"\ensuremath{\mathbf{o}}", # MATHEMATICAL BOLD SMALL o + 0x1D429: r"\ensuremath{\mathbf{p}}", # MATHEMATICAL BOLD SMALL p + 0x1D42A: r"\ensuremath{\mathbf{q}}", # MATHEMATICAL BOLD SMALL q + 0x1D42B: r"\ensuremath{\mathbf{r}}", # MATHEMATICAL BOLD SMALL r + 0x1D42C: r"\ensuremath{\mathbf{s}}", # MATHEMATICAL BOLD SMALL s + 0x1D42D: r"\ensuremath{\mathbf{t}}", # MATHEMATICAL BOLD SMALL t + 0x1D42E: r"\ensuremath{\mathbf{u}}", # MATHEMATICAL BOLD SMALL u + 0x1D42F: r"\ensuremath{\mathbf{v}}", # MATHEMATICAL BOLD SMALL v + 0x1D430: r"\ensuremath{\mathbf{w}}", # MATHEMATICAL BOLD SMALL w + 0x1D431: r"\ensuremath{\mathbf{x}}", # MATHEMATICAL BOLD SMALL x + 0x1D432: r"\ensuremath{\mathbf{y}}", # MATHEMATICAL BOLD SMALL y + 0x1D433: r"\ensuremath{\mathbf{z}}", # MATHEMATICAL BOLD SMALL z + 0x1D434: r"\ensuremath{\mathit{A}}", # MATHEMATICAL ITALIC CAPITAL A + 0x1D435: r"\ensuremath{\mathit{B}}", # MATHEMATICAL ITALIC CAPITAL B + 0x1D436: r"\ensuremath{\mathit{C}}", # MATHEMATICAL ITALIC CAPITAL C + 0x1D437: r"\ensuremath{\mathit{D}}", # MATHEMATICAL ITALIC CAPITAL D + 0x1D438: r"\ensuremath{\mathit{E}}", # MATHEMATICAL ITALIC CAPITAL E + 0x1D439: r"\ensuremath{\mathit{F}}", # MATHEMATICAL ITALIC CAPITAL F + 0x1D43A: r"\ensuremath{\mathit{G}}", # MATHEMATICAL ITALIC CAPITAL G + 0x1D43B: r"\ensuremath{\mathit{H}}", # MATHEMATICAL ITALIC CAPITAL H + 0x1D43C: r"\ensuremath{\mathit{I}}", # MATHEMATICAL ITALIC CAPITAL I + 0x1D43D: r"\ensuremath{\mathit{J}}", # MATHEMATICAL ITALIC CAPITAL J + 0x1D43E: r"\ensuremath{\mathit{K}}", # MATHEMATICAL ITALIC CAPITAL K + 0x1D43F: r"\ensuremath{\mathit{L}}", # MATHEMATICAL ITALIC CAPITAL L + 0x1D440: r"\ensuremath{\mathit{M}}", # MATHEMATICAL ITALIC CAPITAL M + 0x1D441: r"\ensuremath{\mathit{N}}", # MATHEMATICAL ITALIC CAPITAL N + 0x1D442: r"\ensuremath{\mathit{O}}", # MATHEMATICAL ITALIC CAPITAL O + 0x1D443: r"\ensuremath{\mathit{P}}", # MATHEMATICAL ITALIC CAPITAL P + 0x1D444: r"\ensuremath{\mathit{Q}}", # MATHEMATICAL ITALIC CAPITAL Q + 0x1D445: r"\ensuremath{\mathit{R}}", # MATHEMATICAL ITALIC CAPITAL R + 0x1D446: r"\ensuremath{\mathit{S}}", # MATHEMATICAL ITALIC CAPITAL S + 0x1D447: r"\ensuremath{\mathit{T}}", # MATHEMATICAL ITALIC CAPITAL T + 0x1D448: r"\ensuremath{\mathit{U}}", # MATHEMATICAL ITALIC CAPITAL U + 0x1D449: r"\ensuremath{\mathit{V}}", # MATHEMATICAL ITALIC CAPITAL V + 0x1D44A: r"\ensuremath{\mathit{W}}", # MATHEMATICAL ITALIC CAPITAL W + 0x1D44B: r"\ensuremath{\mathit{X}}", # MATHEMATICAL ITALIC CAPITAL X + 0x1D44C: r"\ensuremath{\mathit{Y}}", # MATHEMATICAL ITALIC CAPITAL Y + 0x1D44D: r"\ensuremath{\mathit{Z}}", # MATHEMATICAL ITALIC CAPITAL Z + 0x1D44E: r"\ensuremath{\mathit{a}}", # MATHEMATICAL ITALIC SMALL a + 0x1D44F: r"\ensuremath{\mathit{b}}", # MATHEMATICAL ITALIC SMALL b + 0x1D450: r"\ensuremath{\mathit{c}}", # MATHEMATICAL ITALIC SMALL c + 0x1D451: r"\ensuremath{\mathit{d}}", # MATHEMATICAL ITALIC SMALL d + 0x1D452: r"\ensuremath{\mathit{e}}", # MATHEMATICAL ITALIC SMALL e + 0x1D453: r"\ensuremath{\mathit{f}}", # MATHEMATICAL ITALIC SMALL f + 0x1D454: r"\ensuremath{\mathit{g}}", # MATHEMATICAL ITALIC SMALL g + 0x1D455: r"\ensuremath{\mathit{h}}", # MATHEMATICAL ITALIC SMALL h + 0x1D456: r"\ensuremath{\mathit{i}}", # MATHEMATICAL ITALIC SMALL i + 0x1D457: r"\ensuremath{\mathit{j}}", # MATHEMATICAL ITALIC SMALL j + 0x1D458: r"\ensuremath{\mathit{k}}", # MATHEMATICAL ITALIC SMALL k + 0x1D459: r"\ensuremath{\mathit{l}}", # MATHEMATICAL ITALIC SMALL l + 0x1D45A: r"\ensuremath{\mathit{m}}", # MATHEMATICAL ITALIC SMALL m + 0x1D45B: r"\ensuremath{\mathit{n}}", # MATHEMATICAL ITALIC SMALL n + 0x1D45C: r"\ensuremath{\mathit{o}}", # MATHEMATICAL ITALIC SMALL o + 0x1D45D: r"\ensuremath{\mathit{p}}", # MATHEMATICAL ITALIC SMALL p + 0x1D45E: r"\ensuremath{\mathit{q}}", # MATHEMATICAL ITALIC SMALL q + 0x1D45F: r"\ensuremath{\mathit{r}}", # MATHEMATICAL ITALIC SMALL r + 0x1D460: r"\ensuremath{\mathit{s}}", # MATHEMATICAL ITALIC SMALL s + 0x1D461: r"\ensuremath{\mathit{t}}", # MATHEMATICAL ITALIC SMALL t + 0x1D462: r"\ensuremath{\mathit{u}}", # MATHEMATICAL ITALIC SMALL u + 0x1D463: r"\ensuremath{\mathit{v}}", # MATHEMATICAL ITALIC SMALL v + 0x1D464: r"\ensuremath{\mathit{w}}", # MATHEMATICAL ITALIC SMALL w + 0x1D465: r"\ensuremath{\mathit{x}}", # MATHEMATICAL ITALIC SMALL x + 0x1D466: r"\ensuremath{\mathit{y}}", # MATHEMATICAL ITALIC SMALL y + 0x1D467: r"\ensuremath{\mathit{z}}", # MATHEMATICAL ITALIC SMALL z + 0x1D468: r"\ensuremath{\boldsymbol{\mathit{A}}}", # MATHEMATICAL BOLD ITALIC CAPITAL A + 0x1D469: r"\ensuremath{\boldsymbol{\mathit{B}}}", # MATHEMATICAL BOLD ITALIC CAPITAL B + 0x1D46A: r"\ensuremath{\boldsymbol{\mathit{C}}}", # MATHEMATICAL BOLD ITALIC CAPITAL C + 0x1D46B: r"\ensuremath{\boldsymbol{\mathit{D}}}", # MATHEMATICAL BOLD ITALIC CAPITAL D + 0x1D46C: r"\ensuremath{\boldsymbol{\mathit{E}}}", # MATHEMATICAL BOLD ITALIC CAPITAL E + 0x1D46D: r"\ensuremath{\boldsymbol{\mathit{F}}}", # MATHEMATICAL BOLD ITALIC CAPITAL F + 0x1D46E: r"\ensuremath{\boldsymbol{\mathit{G}}}", # MATHEMATICAL BOLD ITALIC CAPITAL G + 0x1D46F: r"\ensuremath{\boldsymbol{\mathit{H}}}", # MATHEMATICAL BOLD ITALIC CAPITAL H + 0x1D470: r"\ensuremath{\boldsymbol{\mathit{I}}}", # MATHEMATICAL BOLD ITALIC CAPITAL I + 0x1D471: r"\ensuremath{\boldsymbol{\mathit{J}}}", # MATHEMATICAL BOLD ITALIC CAPITAL J + 0x1D472: r"\ensuremath{\boldsymbol{\mathit{K}}}", # MATHEMATICAL BOLD ITALIC CAPITAL K + 0x1D473: r"\ensuremath{\boldsymbol{\mathit{L}}}", # MATHEMATICAL BOLD ITALIC CAPITAL L + 0x1D474: r"\ensuremath{\boldsymbol{\mathit{M}}}", # MATHEMATICAL BOLD ITALIC CAPITAL M + 0x1D475: r"\ensuremath{\boldsymbol{\mathit{N}}}", # MATHEMATICAL BOLD ITALIC CAPITAL N + 0x1D476: r"\ensuremath{\boldsymbol{\mathit{O}}}", # MATHEMATICAL BOLD ITALIC CAPITAL O + 0x1D477: r"\ensuremath{\boldsymbol{\mathit{P}}}", # MATHEMATICAL BOLD ITALIC CAPITAL P + 0x1D478: r"\ensuremath{\boldsymbol{\mathit{Q}}}", # MATHEMATICAL BOLD ITALIC CAPITAL Q + 0x1D479: r"\ensuremath{\boldsymbol{\mathit{R}}}", # MATHEMATICAL BOLD ITALIC CAPITAL R + 0x1D47A: r"\ensuremath{\boldsymbol{\mathit{S}}}", # MATHEMATICAL BOLD ITALIC CAPITAL S + 0x1D47B: r"\ensuremath{\boldsymbol{\mathit{T}}}", # MATHEMATICAL BOLD ITALIC CAPITAL T + 0x1D47C: r"\ensuremath{\boldsymbol{\mathit{U}}}", # MATHEMATICAL BOLD ITALIC CAPITAL U + 0x1D47D: r"\ensuremath{\boldsymbol{\mathit{V}}}", # MATHEMATICAL BOLD ITALIC CAPITAL V + 0x1D47E: r"\ensuremath{\boldsymbol{\mathit{W}}}", # MATHEMATICAL BOLD ITALIC CAPITAL W + 0x1D47F: r"\ensuremath{\boldsymbol{\mathit{X}}}", # MATHEMATICAL BOLD ITALIC CAPITAL X + 0x1D480: r"\ensuremath{\boldsymbol{\mathit{Y}}}", # MATHEMATICAL BOLD ITALIC CAPITAL Y + 0x1D481: r"\ensuremath{\boldsymbol{\mathit{Z}}}", # MATHEMATICAL BOLD ITALIC CAPITAL Z + 0x1D482: r"\ensuremath{\boldsymbol{\mathit{a}}}", # MATHEMATICAL BOLD ITALIC SMALL a + 0x1D483: r"\ensuremath{\boldsymbol{\mathit{b}}}", # MATHEMATICAL BOLD ITALIC SMALL b + 0x1D484: r"\ensuremath{\boldsymbol{\mathit{c}}}", # MATHEMATICAL BOLD ITALIC SMALL c + 0x1D485: r"\ensuremath{\boldsymbol{\mathit{d}}}", # MATHEMATICAL BOLD ITALIC SMALL d + 0x1D486: r"\ensuremath{\boldsymbol{\mathit{e}}}", # MATHEMATICAL BOLD ITALIC SMALL e + 0x1D487: r"\ensuremath{\boldsymbol{\mathit{f}}}", # MATHEMATICAL BOLD ITALIC SMALL f + 0x1D488: r"\ensuremath{\boldsymbol{\mathit{g}}}", # MATHEMATICAL BOLD ITALIC SMALL g + 0x1D489: r"\ensuremath{\boldsymbol{\mathit{h}}}", # MATHEMATICAL BOLD ITALIC SMALL h + 0x1D48A: r"\ensuremath{\boldsymbol{\mathit{i}}}", # MATHEMATICAL BOLD ITALIC SMALL i + 0x1D48B: r"\ensuremath{\boldsymbol{\mathit{j}}}", # MATHEMATICAL BOLD ITALIC SMALL j + 0x1D48C: r"\ensuremath{\boldsymbol{\mathit{k}}}", # MATHEMATICAL BOLD ITALIC SMALL k + 0x1D48D: r"\ensuremath{\boldsymbol{\mathit{l}}}", # MATHEMATICAL BOLD ITALIC SMALL l + 0x1D48E: r"\ensuremath{\boldsymbol{\mathit{m}}}", # MATHEMATICAL BOLD ITALIC SMALL m + 0x1D48F: r"\ensuremath{\boldsymbol{\mathit{n}}}", # MATHEMATICAL BOLD ITALIC SMALL n + 0x1D490: r"\ensuremath{\boldsymbol{\mathit{o}}}", # MATHEMATICAL BOLD ITALIC SMALL o + 0x1D491: r"\ensuremath{\boldsymbol{\mathit{p}}}", # MATHEMATICAL BOLD ITALIC SMALL p + 0x1D492: r"\ensuremath{\boldsymbol{\mathit{q}}}", # MATHEMATICAL BOLD ITALIC SMALL q + 0x1D493: r"\ensuremath{\boldsymbol{\mathit{r}}}", # MATHEMATICAL BOLD ITALIC SMALL r + 0x1D494: r"\ensuremath{\boldsymbol{\mathit{s}}}", # MATHEMATICAL BOLD ITALIC SMALL s + 0x1D495: r"\ensuremath{\boldsymbol{\mathit{t}}}", # MATHEMATICAL BOLD ITALIC SMALL t + 0x1D496: r"\ensuremath{\boldsymbol{\mathit{u}}}", # MATHEMATICAL BOLD ITALIC SMALL u + 0x1D497: r"\ensuremath{\boldsymbol{\mathit{v}}}", # MATHEMATICAL BOLD ITALIC SMALL v + 0x1D498: r"\ensuremath{\boldsymbol{\mathit{w}}}", # MATHEMATICAL BOLD ITALIC SMALL w + 0x1D499: r"\ensuremath{\boldsymbol{\mathit{x}}}", # MATHEMATICAL BOLD ITALIC SMALL x + 0x1D49A: r"\ensuremath{\boldsymbol{\mathit{y}}}", # MATHEMATICAL BOLD ITALIC SMALL y + 0x1D49B: r"\ensuremath{\boldsymbol{\mathit{z}}}", # MATHEMATICAL BOLD ITALIC SMALL z + 0x1D49C: r"\ensuremath{\mathscr{A}}", # MATHEMATICAL SCRIPT CAPITAL A + 0x1D49D: r"\ensuremath{\mathscr{B}}", # MATHEMATICAL SCRIPT CAPITAL B + 0x1D49E: r"\ensuremath{\mathscr{C}}", # MATHEMATICAL SCRIPT CAPITAL C + 0x1D49F: r"\ensuremath{\mathscr{D}}", # MATHEMATICAL SCRIPT CAPITAL D + 0x1D4A0: r"\ensuremath{\mathscr{E}}", # MATHEMATICAL SCRIPT CAPITAL E + 0x1D4A1: r"\ensuremath{\mathscr{F}}", # MATHEMATICAL SCRIPT CAPITAL F + 0x1D4A2: r"\ensuremath{\mathscr{G}}", # MATHEMATICAL SCRIPT CAPITAL G + 0x1D4A3: r"\ensuremath{\mathscr{H}}", # MATHEMATICAL SCRIPT CAPITAL H + 0x1D4A4: r"\ensuremath{\mathscr{I}}", # MATHEMATICAL SCRIPT CAPITAL I + 0x1D4A5: r"\ensuremath{\mathscr{J}}", # MATHEMATICAL SCRIPT CAPITAL J + 0x1D4A6: r"\ensuremath{\mathscr{K}}", # MATHEMATICAL SCRIPT CAPITAL K + 0x1D4A7: r"\ensuremath{\mathscr{L}}", # MATHEMATICAL SCRIPT CAPITAL L + 0x1D4A8: r"\ensuremath{\mathscr{M}}", # MATHEMATICAL SCRIPT CAPITAL M + 0x1D4A9: r"\ensuremath{\mathscr{N}}", # MATHEMATICAL SCRIPT CAPITAL N + 0x1D4AA: r"\ensuremath{\mathscr{O}}", # MATHEMATICAL SCRIPT CAPITAL O + 0x1D4AB: r"\ensuremath{\mathscr{P}}", # MATHEMATICAL SCRIPT CAPITAL P + 0x1D4AC: r"\ensuremath{\mathscr{Q}}", # MATHEMATICAL SCRIPT CAPITAL Q + 0x1D4AD: r"\ensuremath{\mathscr{R}}", # MATHEMATICAL SCRIPT CAPITAL R + 0x1D4AE: r"\ensuremath{\mathscr{S}}", # MATHEMATICAL SCRIPT CAPITAL S + 0x1D4AF: r"\ensuremath{\mathscr{T}}", # MATHEMATICAL SCRIPT CAPITAL T + 0x1D4B0: r"\ensuremath{\mathscr{U}}", # MATHEMATICAL SCRIPT CAPITAL U + 0x1D4B1: r"\ensuremath{\mathscr{V}}", # MATHEMATICAL SCRIPT CAPITAL V + 0x1D4B2: r"\ensuremath{\mathscr{W}}", # MATHEMATICAL SCRIPT CAPITAL W + 0x1D4B3: r"\ensuremath{\mathscr{X}}", # MATHEMATICAL SCRIPT CAPITAL X + 0x1D4B4: r"\ensuremath{\mathscr{Y}}", # MATHEMATICAL SCRIPT CAPITAL Y + 0x1D4B5: r"\ensuremath{\mathscr{Z}}", # MATHEMATICAL SCRIPT CAPITAL Z + 0x1D4B6: r"\ensuremath{\mathscr{a}}", # MATHEMATICAL SCRIPT SMALL A [𝒶] + 0x1D4B7: r"\ensuremath{\mathscr{b}}", # MATHEMATICAL SCRIPT SMALL B [𝒷] + 0x1D4B8: r"\ensuremath{\mathscr{c}}", # MATHEMATICAL SCRIPT SMALL C [𝒸] + 0x1D4B9: r"\ensuremath{\mathscr{d}}", # MATHEMATICAL SCRIPT SMALL D [𝒹] + 0x1D4BB: r"\ensuremath{\mathscr{f}}", # MATHEMATICAL SCRIPT SMALL F [𝒻] + 0x1D4BD: r"\ensuremath{\mathscr{h}}", # MATHEMATICAL SCRIPT SMALL H [𝒽] + 0x1D4BE: r"\ensuremath{\mathscr{i}}", # MATHEMATICAL SCRIPT SMALL I [𝒾] + 0x1D4BF: r"\ensuremath{\mathscr{j}}", # MATHEMATICAL SCRIPT SMALL J [𝒿] + 0x1D4C0: r"\ensuremath{\mathscr{k}}", # MATHEMATICAL SCRIPT SMALL K [𝓀] + 0x1D4C1: r"\ensuremath{\mathscr{l}}", # MATHEMATICAL SCRIPT SMALL L [𝓁] + 0x1D4C2: r"\ensuremath{\mathscr{m}}", # MATHEMATICAL SCRIPT SMALL M [𝓂] + 0x1D4C3: r"\ensuremath{\mathscr{n}}", # MATHEMATICAL SCRIPT SMALL N [𝓃] + 0x1D4C5: r"\ensuremath{\mathscr{p}}", # MATHEMATICAL SCRIPT SMALL P [𝓅] + 0x1D4C6: r"\ensuremath{\mathscr{q}}", # MATHEMATICAL SCRIPT SMALL Q [𝓆] + 0x1D4C7: r"\ensuremath{\mathscr{r}}", # MATHEMATICAL SCRIPT SMALL R [𝓇] + 0x1D4C8: r"\ensuremath{\mathscr{s}}", # MATHEMATICAL SCRIPT SMALL S [𝓈] + 0x1D4C9: r"\ensuremath{\mathscr{t}}", # MATHEMATICAL SCRIPT SMALL T [𝓉] + 0x1D4CA: r"\ensuremath{\mathscr{u}}", # MATHEMATICAL SCRIPT SMALL U [𝓊] + 0x1D4CB: r"\ensuremath{\mathscr{v}}", # MATHEMATICAL SCRIPT SMALL V [𝓋] + 0x1D4CC: r"\ensuremath{\mathscr{w}}", # MATHEMATICAL SCRIPT SMALL W [𝓌] + 0x1D4CD: r"\ensuremath{\mathscr{x}}", # MATHEMATICAL SCRIPT SMALL X [𝓍] + 0x1D4CE: r"\ensuremath{\mathscr{y}}", # MATHEMATICAL SCRIPT SMALL Y [𝓎] + 0x1D4CF: r"\ensuremath{\mathscr{z}}", # MATHEMATICAL SCRIPT SMALL Z [𝓏] + 0x1D504: r"\ensuremath{\mathfrak{A}}", # MATHEMATICAL FRAKTUR CAPITAL A + 0x1D505: r"\ensuremath{\mathfrak{B}}", # MATHEMATICAL FRAKTUR CAPITAL B + 0x1D506: r"\ensuremath{\mathfrak{C}}", # MATHEMATICAL FRAKTUR CAPITAL C + 0x1D507: r"\ensuremath{\mathfrak{D}}", # MATHEMATICAL FRAKTUR CAPITAL D + 0x1D508: r"\ensuremath{\mathfrak{E}}", # MATHEMATICAL FRAKTUR CAPITAL E + 0x1D509: r"\ensuremath{\mathfrak{F}}", # MATHEMATICAL FRAKTUR CAPITAL F + 0x1D50A: r"\ensuremath{\mathfrak{G}}", # MATHEMATICAL FRAKTUR CAPITAL G + 0x1D50B: r"\ensuremath{\mathfrak{H}}", # MATHEMATICAL FRAKTUR CAPITAL H + 0x1D50C: r"\ensuremath{\mathfrak{I}}", # MATHEMATICAL FRAKTUR CAPITAL I + 0x1D50D: r"\ensuremath{\mathfrak{J}}", # MATHEMATICAL FRAKTUR CAPITAL J + 0x1D50E: r"\ensuremath{\mathfrak{K}}", # MATHEMATICAL FRAKTUR CAPITAL K + 0x1D50F: r"\ensuremath{\mathfrak{L}}", # MATHEMATICAL FRAKTUR CAPITAL L + 0x1D510: r"\ensuremath{\mathfrak{M}}", # MATHEMATICAL FRAKTUR CAPITAL M + 0x1D511: r"\ensuremath{\mathfrak{N}}", # MATHEMATICAL FRAKTUR CAPITAL N + 0x1D512: r"\ensuremath{\mathfrak{O}}", # MATHEMATICAL FRAKTUR CAPITAL O + 0x1D513: r"\ensuremath{\mathfrak{P}}", # MATHEMATICAL FRAKTUR CAPITAL P + 0x1D514: r"\ensuremath{\mathfrak{Q}}", # MATHEMATICAL FRAKTUR CAPITAL Q + 0x1D515: r"\ensuremath{\mathfrak{R}}", # MATHEMATICAL FRAKTUR CAPITAL R + 0x1D516: r"\ensuremath{\mathfrak{S}}", # MATHEMATICAL FRAKTUR CAPITAL S + 0x1D517: r"\ensuremath{\mathfrak{T}}", # MATHEMATICAL FRAKTUR CAPITAL T + 0x1D518: r"\ensuremath{\mathfrak{U}}", # MATHEMATICAL FRAKTUR CAPITAL U + 0x1D519: r"\ensuremath{\mathfrak{V}}", # MATHEMATICAL FRAKTUR CAPITAL V + 0x1D51A: r"\ensuremath{\mathfrak{W}}", # MATHEMATICAL FRAKTUR CAPITAL W + 0x1D51B: r"\ensuremath{\mathfrak{X}}", # MATHEMATICAL FRAKTUR CAPITAL X + 0x1D51C: r"\ensuremath{\mathfrak{Y}}", # MATHEMATICAL FRAKTUR CAPITAL Y + 0x1D51D: r"\ensuremath{\mathfrak{Z}}", # MATHEMATICAL FRAKTUR CAPITAL Z + 0x1D51E: r"\ensuremath{\mathfrak{a}}", # MATHEMATICAL FRAKTUR SMALL a + 0x1D51F: r"\ensuremath{\mathfrak{b}}", # MATHEMATICAL FRAKTUR SMALL b + 0x1D520: r"\ensuremath{\mathfrak{c}}", # MATHEMATICAL FRAKTUR SMALL c + 0x1D521: r"\ensuremath{\mathfrak{d}}", # MATHEMATICAL FRAKTUR SMALL d + 0x1D522: r"\ensuremath{\mathfrak{e}}", # MATHEMATICAL FRAKTUR SMALL e + 0x1D523: r"\ensuremath{\mathfrak{f}}", # MATHEMATICAL FRAKTUR SMALL f + 0x1D524: r"\ensuremath{\mathfrak{g}}", # MATHEMATICAL FRAKTUR SMALL g + 0x1D525: r"\ensuremath{\mathfrak{h}}", # MATHEMATICAL FRAKTUR SMALL h + 0x1D526: r"\ensuremath{\mathfrak{i}}", # MATHEMATICAL FRAKTUR SMALL i + 0x1D527: r"\ensuremath{\mathfrak{j}}", # MATHEMATICAL FRAKTUR SMALL j + 0x1D528: r"\ensuremath{\mathfrak{k}}", # MATHEMATICAL FRAKTUR SMALL k + 0x1D529: r"\ensuremath{\mathfrak{l}}", # MATHEMATICAL FRAKTUR SMALL l + 0x1D52A: r"\ensuremath{\mathfrak{m}}", # MATHEMATICAL FRAKTUR SMALL m + 0x1D52B: r"\ensuremath{\mathfrak{n}}", # MATHEMATICAL FRAKTUR SMALL n + 0x1D52C: r"\ensuremath{\mathfrak{o}}", # MATHEMATICAL FRAKTUR SMALL o + 0x1D52D: r"\ensuremath{\mathfrak{p}}", # MATHEMATICAL FRAKTUR SMALL p + 0x1D52E: r"\ensuremath{\mathfrak{q}}", # MATHEMATICAL FRAKTUR SMALL q + 0x1D52F: r"\ensuremath{\mathfrak{r}}", # MATHEMATICAL FRAKTUR SMALL r + 0x1D530: r"\ensuremath{\mathfrak{s}}", # MATHEMATICAL FRAKTUR SMALL s + 0x1D531: r"\ensuremath{\mathfrak{t}}", # MATHEMATICAL FRAKTUR SMALL t + 0x1D532: r"\ensuremath{\mathfrak{u}}", # MATHEMATICAL FRAKTUR SMALL u + 0x1D533: r"\ensuremath{\mathfrak{v}}", # MATHEMATICAL FRAKTUR SMALL v + 0x1D534: r"\ensuremath{\mathfrak{w}}", # MATHEMATICAL FRAKTUR SMALL w + 0x1D535: r"\ensuremath{\mathfrak{x}}", # MATHEMATICAL FRAKTUR SMALL x + 0x1D536: r"\ensuremath{\mathfrak{y}}", # MATHEMATICAL FRAKTUR SMALL y + 0x1D537: r"\ensuremath{\mathfrak{z}}", # MATHEMATICAL FRAKTUR SMALL z + 0x1D538: r"\ensuremath{\mathbb{A}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL A + 0x1D539: r"\ensuremath{\mathbb{B}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL B + 0x1D53A: r"\ensuremath{\mathbb{C}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL C + 0x1D53B: r"\ensuremath{\mathbb{D}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL D + 0x1D53C: r"\ensuremath{\mathbb{E}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL E + 0x1D53D: r"\ensuremath{\mathbb{F}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL F + 0x1D53E: r"\ensuremath{\mathbb{G}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL G + 0x1D53F: r"\ensuremath{\mathbb{H}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL H + 0x1D540: r"\ensuremath{\mathbb{I}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL I + 0x1D541: r"\ensuremath{\mathbb{J}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL J + 0x1D542: r"\ensuremath{\mathbb{K}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL K + 0x1D543: r"\ensuremath{\mathbb{L}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL L + 0x1D544: r"\ensuremath{\mathbb{M}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL M + 0x1D545: r"\ensuremath{\mathbb{N}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL N + 0x1D546: r"\ensuremath{\mathbb{O}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL O + 0x1D547: r"\ensuremath{\mathbb{P}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL P + 0x1D548: r"\ensuremath{\mathbb{Q}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL Q + 0x1D549: r"\ensuremath{\mathbb{R}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL R + 0x1D54A: r"\ensuremath{\mathbb{S}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL S + 0x1D54B: r"\ensuremath{\mathbb{T}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL T + 0x1D54C: r"\ensuremath{\mathbb{U}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL U + 0x1D54D: r"\ensuremath{\mathbb{V}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL V + 0x1D54E: r"\ensuremath{\mathbb{W}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL W + 0x1D54F: r"\ensuremath{\mathbb{X}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL X + 0x1D550: r"\ensuremath{\mathbb{Y}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL Y + 0x1D551: r"\ensuremath{\mathbb{Z}}", # MATHEMATICAL DOUBLE-STRUCK CAPITAL Z + 0x1D552: r"\ensuremath{\mathbb{a}}", # MATHEMATICAL DOUBLE-STRUCK SMALL A [𝕒] + 0x1D553: r"\ensuremath{\mathbb{b}}", # MATHEMATICAL DOUBLE-STRUCK SMALL B [𝕓] + 0x1D554: r"\ensuremath{\mathbb{c}}", # MATHEMATICAL DOUBLE-STRUCK SMALL C [𝕔] + 0x1D555: r"\ensuremath{\mathbb{d}}", # MATHEMATICAL DOUBLE-STRUCK SMALL D [𝕕] + 0x1D556: r"\ensuremath{\mathbb{e}}", # MATHEMATICAL DOUBLE-STRUCK SMALL E [𝕖] + 0x1D557: r"\ensuremath{\mathbb{f}}", # MATHEMATICAL DOUBLE-STRUCK SMALL F [𝕗] + 0x1D558: r"\ensuremath{\mathbb{g}}", # MATHEMATICAL DOUBLE-STRUCK SMALL G [𝕘] + 0x1D559: r"\ensuremath{\mathbb{h}}", # MATHEMATICAL DOUBLE-STRUCK SMALL H [𝕙] + 0x1D55A: r"\ensuremath{\mathbb{i}}", # MATHEMATICAL DOUBLE-STRUCK SMALL I [𝕚] + 0x1D55B: r"\ensuremath{\mathbb{j}}", # MATHEMATICAL DOUBLE-STRUCK SMALL J [𝕛] + 0x1D55C: r"\ensuremath{\mathbb{k}}", # MATHEMATICAL DOUBLE-STRUCK SMALL K [𝕜] + 0x1D55D: r"\ensuremath{\mathbb{l}}", # MATHEMATICAL DOUBLE-STRUCK SMALL L [𝕝] + 0x1D55E: r"\ensuremath{\mathbb{m}}", # MATHEMATICAL DOUBLE-STRUCK SMALL M [𝕞] + 0x1D55F: r"\ensuremath{\mathbb{n}}", # MATHEMATICAL DOUBLE-STRUCK SMALL N [𝕟] + 0x1D560: r"\ensuremath{\mathbb{o}}", # MATHEMATICAL DOUBLE-STRUCK SMALL O [𝕠] + 0x1D561: r"\ensuremath{\mathbb{p}}", # MATHEMATICAL DOUBLE-STRUCK SMALL P [𝕡] + 0x1D562: r"\ensuremath{\mathbb{q}}", # MATHEMATICAL DOUBLE-STRUCK SMALL Q [𝕢] + 0x1D563: r"\ensuremath{\mathbb{r}}", # MATHEMATICAL DOUBLE-STRUCK SMALL R [𝕣] + 0x1D564: r"\ensuremath{\mathbb{s}}", # MATHEMATICAL DOUBLE-STRUCK SMALL S [𝕤] + 0x1D565: r"\ensuremath{\mathbb{t}}", # MATHEMATICAL DOUBLE-STRUCK SMALL T [𝕥] + 0x1D566: r"\ensuremath{\mathbb{u}}", # MATHEMATICAL DOUBLE-STRUCK SMALL U [𝕦] + 0x1D567: r"\ensuremath{\mathbb{v}}", # MATHEMATICAL DOUBLE-STRUCK SMALL V [𝕧] + 0x1D568: r"\ensuremath{\mathbb{w}}", # MATHEMATICAL DOUBLE-STRUCK SMALL W [𝕨] + 0x1D569: r"\ensuremath{\mathbb{x}}", # MATHEMATICAL DOUBLE-STRUCK SMALL X [𝕩] + 0x1D56A: r"\ensuremath{\mathbb{y}}", # MATHEMATICAL DOUBLE-STRUCK SMALL Y [𝕪] + 0x1D56B: r"\ensuremath{\mathbb{z}}", # MATHEMATICAL DOUBLE-STRUCK SMALL Z [𝕫] + 0x1D5A0: r"\ensuremath{\mathsf{A}}", # MATHEMATICAL SANS-SERIF CAPITAL A [𝖠] + 0x1D5A1: r"\ensuremath{\mathsf{B}}", # MATHEMATICAL SANS-SERIF CAPITAL B [𝖡] + 0x1D5A2: r"\ensuremath{\mathsf{C}}", # MATHEMATICAL SANS-SERIF CAPITAL C [𝖢] + 0x1D5A3: r"\ensuremath{\mathsf{D}}", # MATHEMATICAL SANS-SERIF CAPITAL D [𝖣] + 0x1D5A4: r"\ensuremath{\mathsf{E}}", # MATHEMATICAL SANS-SERIF CAPITAL E [𝖤] + 0x1D5A5: r"\ensuremath{\mathsf{F}}", # MATHEMATICAL SANS-SERIF CAPITAL F [𝖥] + 0x1D5A6: r"\ensuremath{\mathsf{G}}", # MATHEMATICAL SANS-SERIF CAPITAL G [𝖦] + 0x1D5A7: r"\ensuremath{\mathsf{H}}", # MATHEMATICAL SANS-SERIF CAPITAL H [𝖧] + 0x1D5A8: r"\ensuremath{\mathsf{I}}", # MATHEMATICAL SANS-SERIF CAPITAL I [𝖨] + 0x1D5A9: r"\ensuremath{\mathsf{J}}", # MATHEMATICAL SANS-SERIF CAPITAL J [𝖩] + 0x1D5AA: r"\ensuremath{\mathsf{K}}", # MATHEMATICAL SANS-SERIF CAPITAL K [𝖪] + 0x1D5AB: r"\ensuremath{\mathsf{L}}", # MATHEMATICAL SANS-SERIF CAPITAL L [𝖫] + 0x1D5AC: r"\ensuremath{\mathsf{M}}", # MATHEMATICAL SANS-SERIF CAPITAL M [𝖬] + 0x1D5AD: r"\ensuremath{\mathsf{N}}", # MATHEMATICAL SANS-SERIF CAPITAL N [𝖭] + 0x1D5AE: r"\ensuremath{\mathsf{O}}", # MATHEMATICAL SANS-SERIF CAPITAL O [𝖮] + 0x1D5AF: r"\ensuremath{\mathsf{P}}", # MATHEMATICAL SANS-SERIF CAPITAL P [𝖯] + 0x1D5B0: r"\ensuremath{\mathsf{Q}}", # MATHEMATICAL SANS-SERIF CAPITAL Q [𝖰] + 0x1D5B1: r"\ensuremath{\mathsf{R}}", # MATHEMATICAL SANS-SERIF CAPITAL R [𝖱] + 0x1D5B2: r"\ensuremath{\mathsf{S}}", # MATHEMATICAL SANS-SERIF CAPITAL S [𝖲] + 0x1D5B3: r"\ensuremath{\mathsf{T}}", # MATHEMATICAL SANS-SERIF CAPITAL T [𝖳] + 0x1D5B4: r"\ensuremath{\mathsf{U}}", # MATHEMATICAL SANS-SERIF CAPITAL U [𝖴] + 0x1D5B5: r"\ensuremath{\mathsf{V}}", # MATHEMATICAL SANS-SERIF CAPITAL V [𝖵] + 0x1D5B6: r"\ensuremath{\mathsf{W}}", # MATHEMATICAL SANS-SERIF CAPITAL W [𝖶] + 0x1D5B7: r"\ensuremath{\mathsf{X}}", # MATHEMATICAL SANS-SERIF CAPITAL X [𝖷] + 0x1D5B8: r"\ensuremath{\mathsf{Y}}", # MATHEMATICAL SANS-SERIF CAPITAL Y [𝖸] + 0x1D5B9: r"\ensuremath{\mathsf{Z}}", # MATHEMATICAL SANS-SERIF CAPITAL Z [𝖹] + 0x1D5BA: r"\ensuremath{\mathsf{a}}", # MATHEMATICAL SANS-SERIF SMALL A [𝖺] + 0x1D5BB: r"\ensuremath{\mathsf{b}}", # MATHEMATICAL SANS-SERIF SMALL B [𝖻] + 0x1D5BC: r"\ensuremath{\mathsf{c}}", # MATHEMATICAL SANS-SERIF SMALL C [𝖼] + 0x1D5BD: r"\ensuremath{\mathsf{d}}", # MATHEMATICAL SANS-SERIF SMALL D [𝖽] + 0x1D5BE: r"\ensuremath{\mathsf{e}}", # MATHEMATICAL SANS-SERIF SMALL E [𝖾] + 0x1D5BF: r"\ensuremath{\mathsf{f}}", # MATHEMATICAL SANS-SERIF SMALL F [𝖿] + 0x1D5C0: r"\ensuremath{\mathsf{g}}", # MATHEMATICAL SANS-SERIF SMALL G [𝗀] + 0x1D5C1: r"\ensuremath{\mathsf{h}}", # MATHEMATICAL SANS-SERIF SMALL H [𝗁] + 0x1D5C2: r"\ensuremath{\mathsf{i}}", # MATHEMATICAL SANS-SERIF SMALL I [𝗂] + 0x1D5C3: r"\ensuremath{\mathsf{j}}", # MATHEMATICAL SANS-SERIF SMALL J [𝗃] + 0x1D5C4: r"\ensuremath{\mathsf{k}}", # MATHEMATICAL SANS-SERIF SMALL K [𝗄] + 0x1D5C5: r"\ensuremath{\mathsf{l}}", # MATHEMATICAL SANS-SERIF SMALL L [𝗅] + 0x1D5C6: r"\ensuremath{\mathsf{m}}", # MATHEMATICAL SANS-SERIF SMALL M [𝗆] + 0x1D5C7: r"\ensuremath{\mathsf{n}}", # MATHEMATICAL SANS-SERIF SMALL N [𝗇] + 0x1D5C8: r"\ensuremath{\mathsf{o}}", # MATHEMATICAL SANS-SERIF SMALL O [𝗈] + 0x1D5C9: r"\ensuremath{\mathsf{p}}", # MATHEMATICAL SANS-SERIF SMALL P [𝗉] + 0x1D5CA: r"\ensuremath{\mathsf{q}}", # MATHEMATICAL SANS-SERIF SMALL Q [𝗊] + 0x1D5CB: r"\ensuremath{\mathsf{r}}", # MATHEMATICAL SANS-SERIF SMALL R [𝗋] + 0x1D5CC: r"\ensuremath{\mathsf{s}}", # MATHEMATICAL SANS-SERIF SMALL S [𝗌] + 0x1D5CD: r"\ensuremath{\mathsf{t}}", # MATHEMATICAL SANS-SERIF SMALL T [𝗍] + 0x1D5CE: r"\ensuremath{\mathsf{u}}", # MATHEMATICAL SANS-SERIF SMALL U [𝗎] + 0x1D5CF: r"\ensuremath{\mathsf{v}}", # MATHEMATICAL SANS-SERIF SMALL V [𝗏] + 0x1D5D0: r"\ensuremath{\mathsf{w}}", # MATHEMATICAL SANS-SERIF SMALL W [𝗐] + 0x1D5D1: r"\ensuremath{\mathsf{x}}", # MATHEMATICAL SANS-SERIF SMALL X [𝗑] + 0x1D5D2: r"\ensuremath{\mathsf{y}}", # MATHEMATICAL SANS-SERIF SMALL Y [𝗒] + 0x1D5D3: r"\ensuremath{\mathsf{z}}", # MATHEMATICAL SANS-SERIF SMALL Z [𝗓] + 0x1D670: r"\ensuremath{\mathtt{A}}", # MATHEMATICAL MONOSPACE CAPITAL A [𝙰] + 0x1D671: r"\ensuremath{\mathtt{B}}", # MATHEMATICAL MONOSPACE CAPITAL B [𝙱] + 0x1D672: r"\ensuremath{\mathtt{C}}", # MATHEMATICAL MONOSPACE CAPITAL C [𝙲] + 0x1D673: r"\ensuremath{\mathtt{D}}", # MATHEMATICAL MONOSPACE CAPITAL D [𝙳] + 0x1D674: r"\ensuremath{\mathtt{E}}", # MATHEMATICAL MONOSPACE CAPITAL E [𝙴] + 0x1D675: r"\ensuremath{\mathtt{F}}", # MATHEMATICAL MONOSPACE CAPITAL F [𝙵] + 0x1D676: r"\ensuremath{\mathtt{G}}", # MATHEMATICAL MONOSPACE CAPITAL G [𝙶] + 0x1D677: r"\ensuremath{\mathtt{H}}", # MATHEMATICAL MONOSPACE CAPITAL H [𝙷] + 0x1D678: r"\ensuremath{\mathtt{I}}", # MATHEMATICAL MONOSPACE CAPITAL I [𝙸] + 0x1D679: r"\ensuremath{\mathtt{J}}", # MATHEMATICAL MONOSPACE CAPITAL J [𝙹] + 0x1D67A: r"\ensuremath{\mathtt{K}}", # MATHEMATICAL MONOSPACE CAPITAL K [𝙺] + 0x1D67B: r"\ensuremath{\mathtt{L}}", # MATHEMATICAL MONOSPACE CAPITAL L [𝙻] + 0x1D67C: r"\ensuremath{\mathtt{M}}", # MATHEMATICAL MONOSPACE CAPITAL M [𝙼] + 0x1D67D: r"\ensuremath{\mathtt{N}}", # MATHEMATICAL MONOSPACE CAPITAL N [𝙽] + 0x1D67E: r"\ensuremath{\mathtt{O}}", # MATHEMATICAL MONOSPACE CAPITAL O [𝙾] + 0x1D67F: r"\ensuremath{\mathtt{P}}", # MATHEMATICAL MONOSPACE CAPITAL P [𝙿] + 0x1D680: r"\ensuremath{\mathtt{Q}}", # MATHEMATICAL MONOSPACE CAPITAL Q [𝚀] + 0x1D681: r"\ensuremath{\mathtt{R}}", # MATHEMATICAL MONOSPACE CAPITAL R [𝚁] + 0x1D682: r"\ensuremath{\mathtt{S}}", # MATHEMATICAL MONOSPACE CAPITAL S [𝚂] + 0x1D683: r"\ensuremath{\mathtt{T}}", # MATHEMATICAL MONOSPACE CAPITAL T [𝚃] + 0x1D684: r"\ensuremath{\mathtt{U}}", # MATHEMATICAL MONOSPACE CAPITAL U [𝚄] + 0x1D685: r"\ensuremath{\mathtt{V}}", # MATHEMATICAL MONOSPACE CAPITAL V [𝚅] + 0x1D686: r"\ensuremath{\mathtt{W}}", # MATHEMATICAL MONOSPACE CAPITAL W [𝚆] + 0x1D687: r"\ensuremath{\mathtt{X}}", # MATHEMATICAL MONOSPACE CAPITAL X [𝚇] + 0x1D688: r"\ensuremath{\mathtt{Y}}", # MATHEMATICAL MONOSPACE CAPITAL Y [𝚈] + 0x1D689: r"\ensuremath{\mathtt{Z}}", # MATHEMATICAL MONOSPACE CAPITAL Z [𝚉] + 0x1D68A: r"\ensuremath{\mathtt{a}}", # MATHEMATICAL MONOSPACE SMALL A [𝚊] + 0x1D68B: r"\ensuremath{\mathtt{b}}", # MATHEMATICAL MONOSPACE SMALL B [𝚋] + 0x1D68C: r"\ensuremath{\mathtt{c}}", # MATHEMATICAL MONOSPACE SMALL C [𝚌] + 0x1D68D: r"\ensuremath{\mathtt{d}}", # MATHEMATICAL MONOSPACE SMALL D [𝚍] + 0x1D68E: r"\ensuremath{\mathtt{e}}", # MATHEMATICAL MONOSPACE SMALL E [𝚎] + 0x1D68F: r"\ensuremath{\mathtt{f}}", # MATHEMATICAL MONOSPACE SMALL F [𝚏] + 0x1D690: r"\ensuremath{\mathtt{g}}", # MATHEMATICAL MONOSPACE SMALL G [𝚐] + 0x1D691: r"\ensuremath{\mathtt{h}}", # MATHEMATICAL MONOSPACE SMALL H [𝚑] + 0x1D692: r"\ensuremath{\mathtt{i}}", # MATHEMATICAL MONOSPACE SMALL I [𝚒] + 0x1D693: r"\ensuremath{\mathtt{j}}", # MATHEMATICAL MONOSPACE SMALL J [𝚓] + 0x1D694: r"\ensuremath{\mathtt{k}}", # MATHEMATICAL MONOSPACE SMALL K [𝚔] + 0x1D695: r"\ensuremath{\mathtt{l}}", # MATHEMATICAL MONOSPACE SMALL L [𝚕] + 0x1D696: r"\ensuremath{\mathtt{m}}", # MATHEMATICAL MONOSPACE SMALL M [𝚖] + 0x1D697: r"\ensuremath{\mathtt{n}}", # MATHEMATICAL MONOSPACE SMALL N [𝚗] + 0x1D698: r"\ensuremath{\mathtt{o}}", # MATHEMATICAL MONOSPACE SMALL O [𝚘] + 0x1D699: r"\ensuremath{\mathtt{p}}", # MATHEMATICAL MONOSPACE SMALL P [𝚙] + 0x1D69A: r"\ensuremath{\mathtt{q}}", # MATHEMATICAL MONOSPACE SMALL Q [𝚚] + 0x1D69B: r"\ensuremath{\mathtt{r}}", # MATHEMATICAL MONOSPACE SMALL R [𝚛] + 0x1D69C: r"\ensuremath{\mathtt{s}}", # MATHEMATICAL MONOSPACE SMALL S [𝚜] + 0x1D69D: r"\ensuremath{\mathtt{t}}", # MATHEMATICAL MONOSPACE SMALL T [𝚝] + 0x1D69E: r"\ensuremath{\mathtt{u}}", # MATHEMATICAL MONOSPACE SMALL U [𝚞] + 0x1D69F: r"\ensuremath{\mathtt{v}}", # MATHEMATICAL MONOSPACE SMALL V [𝚟] + 0x1D6A0: r"\ensuremath{\mathtt{w}}", # MATHEMATICAL MONOSPACE SMALL W [𝚠] + 0x1D6A1: r"\ensuremath{\mathtt{x}}", # MATHEMATICAL MONOSPACE SMALL X [𝚡] + 0x1D6A2: r"\ensuremath{\mathtt{y}}", # MATHEMATICAL MONOSPACE SMALL Y [𝚢] + 0x1D6A3: r"\ensuremath{\mathtt{z}}", # MATHEMATICAL MONOSPACE SMALL Z [𝚣] + 0x1D7CE: r"\ensuremath{\mathbf{0}}", # MATHEMATICAL BOLD DIGIT ZERO [𝟎] + 0x1D7CF: r"\ensuremath{\mathbf{1}}", # MATHEMATICAL BOLD DIGIT ONE [𝟏] + 0x1D7D0: r"\ensuremath{\mathbf{2}}", # MATHEMATICAL BOLD DIGIT TWO [𝟐] + 0x1D7D1: r"\ensuremath{\mathbf{3}}", # MATHEMATICAL BOLD DIGIT THREE [𝟑] + 0x1D7D2: r"\ensuremath{\mathbf{4}}", # MATHEMATICAL BOLD DIGIT FOUR [𝟒] + 0x1D7D3: r"\ensuremath{\mathbf{5}}", # MATHEMATICAL BOLD DIGIT FIVE [𝟓] + 0x1D7D4: r"\ensuremath{\mathbf{6}}", # MATHEMATICAL BOLD DIGIT SIX [𝟔] + 0x1D7D5: r"\ensuremath{\mathbf{7}}", # MATHEMATICAL BOLD DIGIT SEVEN [𝟕] + 0x1D7D6: r"\ensuremath{\mathbf{8}}", # MATHEMATICAL BOLD DIGIT EIGHT [𝟖] + 0x1D7D7: r"\ensuremath{\mathbf{9}}", # MATHEMATICAL BOLD DIGIT NINE [𝟗] + 0x1D7D8: r"\ensuremath{\mathbb{0}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT ZERO [𝟘] + 0x1D7D9: r"\ensuremath{\mathbb{1}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT ONE [𝟙] + 0x1D7DA: r"\ensuremath{\mathbb{2}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT TWO [𝟚] + 0x1D7DB: r"\ensuremath{\mathbb{3}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT THREE [𝟛] + 0x1D7DC: r"\ensuremath{\mathbb{4}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT FOUR [𝟜] + 0x1D7DD: r"\ensuremath{\mathbb{5}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT FIVE [𝟝] + 0x1D7DE: r"\ensuremath{\mathbb{6}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT SIX [𝟞] + 0x1D7DF: r"\ensuremath{\mathbb{7}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT SEVEN [𝟟] + 0x1D7E0: r"\ensuremath{\mathbb{8}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT EIGHT [𝟠] + 0x1D7E1: r"\ensuremath{\mathbb{9}}", # MATHEMATICAL DOUBLE-STRUCK DIGIT NINE [𝟡] + 0x1D7E2: r"\ensuremath{\mathsf{0}}", # MATHEMATICAL SANS-SERIF DIGIT ZERO [𝟢] + 0x1D7E3: r"\ensuremath{\mathsf{1}}", # MATHEMATICAL SANS-SERIF DIGIT ONE [𝟣] + 0x1D7E4: r"\ensuremath{\mathsf{2}}", # MATHEMATICAL SANS-SERIF DIGIT TWO [𝟤] + 0x1D7E5: r"\ensuremath{\mathsf{3}}", # MATHEMATICAL SANS-SERIF DIGIT THREE [𝟥] + 0x1D7E6: r"\ensuremath{\mathsf{4}}", # MATHEMATICAL SANS-SERIF DIGIT FOUR [𝟦] + 0x1D7E7: r"\ensuremath{\mathsf{5}}", # MATHEMATICAL SANS-SERIF DIGIT FIVE [𝟧] + 0x1D7E8: r"\ensuremath{\mathsf{6}}", # MATHEMATICAL SANS-SERIF DIGIT SIX [𝟨] + 0x1D7E9: r"\ensuremath{\mathsf{7}}", # MATHEMATICAL SANS-SERIF DIGIT SEVEN [𝟩] + 0x1D7EA: r"\ensuremath{\mathsf{8}}", # MATHEMATICAL SANS-SERIF DIGIT EIGHT [𝟪] + 0x1D7EB: r"\ensuremath{\mathsf{9}}", # MATHEMATICAL SANS-SERIF DIGIT NINE [𝟫] + 0x1D7F6: r"\ensuremath{\mathtt{0}}", # MATHEMATICAL MONOSPACE DIGIT ZERO [𝟶] + 0x1D7F7: r"\ensuremath{\mathtt{1}}", # MATHEMATICAL MONOSPACE DIGIT ONE [𝟷] + 0x1D7F8: r"\ensuremath{\mathtt{2}}", # MATHEMATICAL MONOSPACE DIGIT TWO [𝟸] + 0x1D7F9: r"\ensuremath{\mathtt{3}}", # MATHEMATICAL MONOSPACE DIGIT THREE [𝟹] + 0x1D7FA: r"\ensuremath{\mathtt{4}}", # MATHEMATICAL MONOSPACE DIGIT FOUR [𝟺] + 0x1D7FB: r"\ensuremath{\mathtt{5}}", # MATHEMATICAL MONOSPACE DIGIT FIVE [𝟻] + 0x1D7FC: r"\ensuremath{\mathtt{6}}", # MATHEMATICAL MONOSPACE DIGIT SIX [𝟼] + 0x1D7FD: r"\ensuremath{\mathtt{7}}", # MATHEMATICAL MONOSPACE DIGIT SEVEN [𝟽] + 0x1D7FE: r"\ensuremath{\mathtt{8}}", # MATHEMATICAL MONOSPACE DIGIT EIGHT [𝟾] + 0x1D7FF: r"\ensuremath{\mathtt{9}}", # MATHEMATICAL MONOSPACE DIGIT NINE [𝟿] +} diff --git a/good_ai/src/sus/external/pylatexenc/latexencode/_uni2latexmap_xml.py b/good_ai/src/sus/external/pylatexenc/latexencode/_uni2latexmap_xml.py new file mode 100644 index 0000000..87763a0 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexencode/_uni2latexmap_xml.py @@ -0,0 +1,2240 @@ +# -*- coding: utf-8 -*- +# +# Automatically generated from unicode.xml by gen_xml_dic.py +# + +uni2latex = { + 0x0023: "\\#", + 0x0024: "\\textdollar", + 0x0025: "\\%", + 0x0026: "\\&", + 0x0027: "\\textquotesingle", + 0x002A: "\\ast", + 0x005C: "\\textbackslash", + 0x005E: "\\^{}", + 0x005F: "\\_", + 0x0060: "\\textasciigrave", + 0x007B: "\\lbrace", + 0x007C: "\\vert", + 0x007D: "\\rbrace", + 0x007E: "\\textasciitilde", + 0x00A0: "~", + 0x00A1: "\\textexclamdown", + 0x00A2: "\\textcent", + 0x00A3: "\\textsterling", + 0x00A4: "\\textcurrency", + 0x00A5: "\\textyen", + 0x00A6: "\\textbrokenbar", + 0x00A7: "\\textsection", + 0x00A8: "\\textasciidieresis", + 0x00A9: "\\textcopyright", + 0x00AA: "\\textordfeminine", + 0x00AB: "\\guillemotleft", + 0x00AC: "\\lnot", + 0x00AD: "\\-", + 0x00AE: "\\textregistered", + 0x00AF: "\\textasciimacron", + 0x00B0: "\\textdegree", + 0x00B1: "\\pm", + 0x00B2: "{^2}", + 0x00B3: "{^3}", + 0x00B4: "\\textasciiacute", + 0x00B5: "\\mathrm{\\mu}", + 0x00B6: "\\textparagraph", + 0x00B7: "\\cdot", + 0x00B8: "\\c{}", + 0x00B9: "{^1}", + 0x00BA: "\\textordmasculine", + 0x00BB: "\\guillemotright", + 0x00BC: "\\textonequarter", + 0x00BD: "\\textonehalf", + 0x00BE: "\\textthreequarters", + 0x00BF: "\\textquestiondown", + 0x00C0: "\\`{A}", + 0x00C1: "\\'{A}", + 0x00C2: "\\^{A}", + 0x00C3: "\\~{A}", + 0x00C4: '\\"{A}', + 0x00C5: "\\AA", + 0x00C6: "\\AE", + 0x00C7: "\\c{C}", + 0x00C8: "\\`{E}", + 0x00C9: "\\'{E}", + 0x00CA: "\\^{E}", + 0x00CB: '\\"{E}', + 0x00CC: "\\`{I}", + 0x00CD: "\\'{I}", + 0x00CE: "\\^{I}", + 0x00CF: '\\"{I}', + 0x00D0: "\\DH", + 0x00D1: "\\~{N}", + 0x00D2: "\\`{O}", + 0x00D3: "\\'{O}", + 0x00D4: "\\^{O}", + 0x00D5: "\\~{O}", + 0x00D6: '\\"{O}', + 0x00D7: "\\texttimes", + 0x00D8: "\\O", + 0x00D9: "\\`{U}", + 0x00DA: "\\'{U}", + 0x00DB: "\\^{U}", + 0x00DC: '\\"{U}', + 0x00DD: "\\'{Y}", + 0x00DE: "\\TH", + 0x00DF: "\\ss", + 0x00E0: "\\`{a}", + 0x00E1: "\\'{a}", + 0x00E2: "\\^{a}", + 0x00E3: "\\~{a}", + 0x00E4: '\\"{a}', + 0x00E5: "\\aa", + 0x00E6: "\\ae", + 0x00E7: "\\c{c}", + 0x00E8: "\\`{e}", + 0x00E9: "\\'{e}", + 0x00EA: "\\^{e}", + 0x00EB: '\\"{e}', + 0x00EC: "\\`{\\i}", + 0x00ED: "\\'{\\i}", + 0x00EE: "\\^{\\i}", + 0x00EF: '\\"{\\i}', + 0x00F0: "\\dh", + 0x00F1: "\\~{n}", + 0x00F2: "\\`{o}", + 0x00F3: "\\'{o}", + 0x00F4: "\\^{o}", + 0x00F5: "\\~{o}", + 0x00F6: '\\"{o}', + 0x00F7: "\\div", + 0x00F8: "\\o", + 0x00F9: "\\`{u}", + 0x00FA: "\\'{u}", + 0x00FB: "\\^{u}", + 0x00FC: '\\"{u}', + 0x00FD: "\\'{y}", + 0x00FE: "\\th", + 0x00FF: '\\"{y}', + 0x0100: "\\={A}", + 0x0101: "\\={a}", + 0x0102: "\\u{A}", + 0x0103: "\\u{a}", + 0x0104: "\\k{A}", + 0x0105: "\\k{a}", + 0x0106: "\\'{C}", + 0x0107: "\\'{c}", + 0x0108: "\\^{C}", + 0x0109: "\\^{c}", + 0x010A: "\\.{C}", + 0x010B: "\\.{c}", + 0x010C: "\\v{C}", + 0x010D: "\\v{c}", + 0x010E: "\\v{D}", + 0x010F: "\\v{d}", + 0x0110: "\\DJ", + 0x0111: "\\dj", + 0x0112: "\\={E}", + 0x0113: "\\={e}", + 0x0114: "\\u{E}", + 0x0115: "\\u{e}", + 0x0116: "\\.{E}", + 0x0117: "\\.{e}", + 0x0118: "\\k{E}", + 0x0119: "\\k{e}", + 0x011A: "\\v{E}", + 0x011B: "\\v{e}", + 0x011C: "\\^{G}", + 0x011D: "\\^{g}", + 0x011E: "\\u{G}", + 0x011F: "\\u{g}", + 0x0120: "\\.{G}", + 0x0121: "\\.{g}", + 0x0122: "\\c{G}", + 0x0123: "\\c{g}", + 0x0124: "\\^{H}", + 0x0125: "\\^{h}", + 0x0126: "{\\fontencoding{LELA}\\selectfont\\char40}", + 0x0128: "\\~{I}", + 0x0129: "\\~{\\i}", + 0x012A: "\\={I}", + 0x012B: "\\={\\i}", + 0x012C: "\\u{I}", + 0x012D: "\\u{\\i}", + 0x012E: "\\k{I}", + 0x012F: "\\k{i}", + 0x0130: "\\.{I}", + 0x0131: "\\i", + 0x0132: "IJ", + 0x0133: "ij", + 0x0134: "\\^{J}", + 0x0135: "\\^{\\j}", + 0x0136: "\\c{K}", + 0x0137: "\\c{k}", + 0x0138: "{\\fontencoding{LELA}\\selectfont\\char91}", + 0x0139: "\\'{L}", + 0x013A: "\\'{l}", + 0x013B: "\\c{L}", + 0x013C: "\\c{l}", + 0x013D: "\\v{L}", + 0x013E: "\\v{l}", + 0x013F: "{\\fontencoding{LELA}\\selectfont\\char201}", + 0x0140: "{\\fontencoding{LELA}\\selectfont\\char202}", + 0x0141: "\\L", + 0x0142: "\\l", + 0x0143: "\\'{N}", + 0x0144: "\\'{n}", + 0x0145: "\\c{N}", + 0x0146: "\\c{n}", + 0x0147: "\\v{N}", + 0x0148: "\\v{n}", + 0x0149: "'n", + 0x014A: "\\NG", + 0x014B: "\\ng", + 0x014C: "\\={O}", + 0x014D: "\\={o}", + 0x014E: "\\u{O}", + 0x014F: "\\u{o}", + 0x0150: "\\H{O}", + 0x0151: "\\H{o}", + 0x0152: "\\OE", + 0x0153: "\\oe", + 0x0154: "\\'{R}", + 0x0155: "\\'{r}", + 0x0156: "\\c{R}", + 0x0157: "\\c{r}", + 0x0158: "\\v{R}", + 0x0159: "\\v{r}", + 0x015A: "\\'{S}", + 0x015B: "\\'{s}", + 0x015C: "\\^{S}", + 0x015D: "\\^{s}", + 0x015E: "\\c{S}", + 0x015F: "\\c{s}", + 0x0160: "\\v{S}", + 0x0161: "\\v{s}", + 0x0162: "\\c{T}", + 0x0163: "\\c{t}", + 0x0164: "\\v{T}", + 0x0165: "\\v{t}", + 0x0166: "{\\fontencoding{LELA}\\selectfont\\char47}", + 0x0167: "{\\fontencoding{LELA}\\selectfont\\char63}", + 0x0168: "\\~{U}", + 0x0169: "\\~{u}", + 0x016A: "\\={U}", + 0x016B: "\\={u}", + 0x016C: "\\u{U}", + 0x016D: "\\u{u}", + 0x016E: "\\r{U}", + 0x016F: "\\r{u}", + 0x0170: "\\H{U}", + 0x0171: "\\H{u}", + 0x0172: "\\k{U}", + 0x0173: "\\k{u}", + 0x0174: "\\^{W}", + 0x0175: "\\^{w}", + 0x0176: "\\^{Y}", + 0x0177: "\\^{y}", + 0x0178: '\\"{Y}', + 0x0179: "\\'{Z}", + 0x017A: "\\'{z}", + 0x017B: "\\.{Z}", + 0x017C: "\\.{z}", + 0x017D: "\\v{Z}", + 0x017E: "\\v{z}", + 0x0192: "f", + 0x0195: "\\texthvlig", + 0x019E: "\\textnrleg", + 0x01AA: "\\eth", + 0x01BA: "{\\fontencoding{LELA}\\selectfont\\char195}", + 0x01C2: "\\textdoublepipe", + 0x01F5: "\\'{g}", + 0x0258: "{\\fontencoding{LEIP}\\selectfont\\char61}", + 0x025B: "\\varepsilon", + 0x0261: "g", + 0x0278: "\\textphi", + 0x027F: "{\\fontencoding{LEIP}\\selectfont\\char202}", + 0x029E: "\\textturnk", + 0x02BC: "'", + 0x02C7: "\\textasciicaron", + 0x02D8: "\\textasciibreve", + 0x02D9: "\\textperiodcentered", + 0x02DA: "\\r{}", + 0x02DB: "\\k{}", + 0x02DC: "\\texttildelow", + 0x02DD: "\\H{}", + 0x02E5: "\\tone{55}", + 0x02E6: "\\tone{44}", + 0x02E7: "\\tone{33}", + 0x02E8: "\\tone{22}", + 0x02E9: "\\tone{11}", + 0x0300: "\\`", + 0x0301: "\\'", + 0x0302: "\\^", + 0x0303: "\\~", + 0x0304: "\\=", + 0x0306: "\\u", + 0x0307: "\\.", + 0x0308: '\\"', + 0x030A: "\\r", + 0x030B: "\\H", + 0x030C: "\\v", + 0x030F: "\\cyrchar\\C", + 0x0311: "{\\fontencoding{LECO}\\selectfont\\char177}", + 0x0318: "{\\fontencoding{LECO}\\selectfont\\char184}", + 0x0319: "{\\fontencoding{LECO}\\selectfont\\char185}", + 0x0327: "\\c", + 0x0328: "\\k", + 0x032B: "{\\fontencoding{LECO}\\selectfont\\char203}", + 0x032F: "{\\fontencoding{LECO}\\selectfont\\char207}", + 0x0337: "{\\fontencoding{LECO}\\selectfont\\char215}", + 0x0338: "{\\fontencoding{LECO}\\selectfont\\char216}", + 0x033A: "{\\fontencoding{LECO}\\selectfont\\char218}", + 0x033B: "{\\fontencoding{LECO}\\selectfont\\char219}", + 0x033C: "{\\fontencoding{LECO}\\selectfont\\char220}", + 0x033D: "{\\fontencoding{LECO}\\selectfont\\char221}", + 0x0361: "{\\fontencoding{LECO}\\selectfont\\char225}", + 0x0386: "\\'{A}", + 0x0388: "\\'{E}", + 0x0389: "\\'{H}", + 0x038A: "\\'{}{I}", + 0x038C: "\\'{}O", + 0x038E: "\\mathrm{'Y}", + 0x038F: "\\mathrm{'\\Omega}", + 0x0390: "\\acute{\\ddot{\\iota}}", + 0x0391: "\\Alpha", + 0x0392: "\\Beta", + 0x0393: "\\Gamma", + 0x0394: "\\Delta", + 0x0395: "\\Epsilon", + 0x0396: "\\Zeta", + 0x0397: "\\Eta", + 0x0398: "\\Theta", + 0x0399: "\\Iota", + 0x039A: "\\Kappa", + 0x039B: "\\Lambda", + 0x039C: "M", + 0x039D: "N", + 0x039E: "\\Xi", + 0x039F: "O", + 0x03A0: "\\Pi", + 0x03A1: "\\Rho", + 0x03A3: "\\Sigma", + 0x03A4: "\\Tau", + 0x03A5: "\\Upsilon", + 0x03A6: "\\Phi", + 0x03A7: "\\Chi", + 0x03A8: "\\Psi", + 0x03A9: "\\Omega", + 0x03AA: "\\mathrm{\\ddot{I}}", + 0x03AB: "\\mathrm{\\ddot{Y}}", + 0x03AC: "\\'{$\\alpha$}", + 0x03AD: "\\acute{\\epsilon}", + 0x03AE: "\\acute{\\eta}", + 0x03AF: "\\acute{\\iota}", + 0x03B0: "\\acute{\\ddot{\\upsilon}}", + 0x03B1: "\\alpha", + 0x03B2: "\\beta", + 0x03B3: "\\gamma", + 0x03B4: "\\delta", + 0x03B5: "\\epsilon", + 0x03B6: "\\zeta", + 0x03B7: "\\eta", + 0x03B8: "\\texttheta", + 0x03B9: "\\iota", + 0x03BA: "\\kappa", + 0x03BB: "\\lambda", + 0x03BC: "\\mu", + 0x03BD: "\\nu", + 0x03BE: "\\xi", + 0x03BF: "o", + 0x03C0: "\\pi", + 0x03C1: "\\rho", + 0x03C2: "\\varsigma", + 0x03C3: "\\sigma", + 0x03C4: "\\tau", + 0x03C5: "\\upsilon", + 0x03C6: "\\varphi", + 0x03C7: "\\chi", + 0x03C8: "\\psi", + 0x03C9: "\\omega", + 0x03CA: "\\ddot{\\iota}", + 0x03CB: "\\ddot{\\upsilon}", + 0x03CC: "\\'{o}", + 0x03CD: "\\acute{\\upsilon}", + 0x03CE: "\\acute{\\omega}", + 0x03D0: "\\Pisymbol{ppi022}{87}", + 0x03D1: "\\textvartheta", + 0x03D2: "\\Upsilon", + 0x03D5: "\\phi", + 0x03D6: "\\varpi", + 0x03DA: "\\Stigma", + 0x03DC: "\\Digamma", + 0x03DD: "\\digamma", + 0x03DE: "\\Koppa", + 0x03E0: "\\Sampi", + 0x03F0: "\\varkappa", + 0x03F1: "\\varrho", + 0x03F4: "\\textTheta", + 0x03F6: "\\backepsilon", + 0x0401: "\\cyrchar\\CYRYO", + 0x0402: "\\cyrchar\\CYRDJE", + 0x0403: "\\cyrchar{\\'\\CYRG}", + 0x0404: "\\cyrchar\\CYRIE", + 0x0405: "\\cyrchar\\CYRDZE", + 0x0406: "\\cyrchar\\CYRII", + 0x0407: "\\cyrchar\\CYRYI", + 0x0408: "\\cyrchar\\CYRJE", + 0x0409: "\\cyrchar\\CYRLJE", + 0x040A: "\\cyrchar\\CYRNJE", + 0x040B: "\\cyrchar\\CYRTSHE", + 0x040C: "\\cyrchar{\\'\\CYRK}", + 0x040E: "\\cyrchar\\CYRUSHRT", + 0x040F: "\\cyrchar\\CYRDZHE", + 0x0410: "\\cyrchar\\CYRA", + 0x0411: "\\cyrchar\\CYRB", + 0x0412: "\\cyrchar\\CYRV", + 0x0413: "\\cyrchar\\CYRG", + 0x0414: "\\cyrchar\\CYRD", + 0x0415: "\\cyrchar\\CYRE", + 0x0416: "\\cyrchar\\CYRZH", + 0x0417: "\\cyrchar\\CYRZ", + 0x0418: "\\cyrchar\\CYRI", + 0x0419: "\\cyrchar\\CYRISHRT", + 0x041A: "\\cyrchar\\CYRK", + 0x041B: "\\cyrchar\\CYRL", + 0x041C: "\\cyrchar\\CYRM", + 0x041D: "\\cyrchar\\CYRN", + 0x041E: "\\cyrchar\\CYRO", + 0x041F: "\\cyrchar\\CYRP", + 0x0420: "\\cyrchar\\CYRR", + 0x0421: "\\cyrchar\\CYRS", + 0x0422: "\\cyrchar\\CYRT", + 0x0423: "\\cyrchar\\CYRU", + 0x0424: "\\cyrchar\\CYRF", + 0x0425: "\\cyrchar\\CYRH", + 0x0426: "\\cyrchar\\CYRC", + 0x0427: "\\cyrchar\\CYRCH", + 0x0428: "\\cyrchar\\CYRSH", + 0x0429: "\\cyrchar\\CYRSHCH", + 0x042A: "\\cyrchar\\CYRHRDSN", + 0x042B: "\\cyrchar\\CYRERY", + 0x042C: "\\cyrchar\\CYRSFTSN", + 0x042D: "\\cyrchar\\CYREREV", + 0x042E: "\\cyrchar\\CYRYU", + 0x042F: "\\cyrchar\\CYRYA", + 0x0430: "\\cyrchar\\cyra", + 0x0431: "\\cyrchar\\cyrb", + 0x0432: "\\cyrchar\\cyrv", + 0x0433: "\\cyrchar\\cyrg", + 0x0434: "\\cyrchar\\cyrd", + 0x0435: "\\cyrchar\\cyre", + 0x0436: "\\cyrchar\\cyrzh", + 0x0437: "\\cyrchar\\cyrz", + 0x0438: "\\cyrchar\\cyri", + 0x0439: "\\cyrchar\\cyrishrt", + 0x043A: "\\cyrchar\\cyrk", + 0x043B: "\\cyrchar\\cyrl", + 0x043C: "\\cyrchar\\cyrm", + 0x043D: "\\cyrchar\\cyrn", + 0x043E: "\\cyrchar\\cyro", + 0x043F: "\\cyrchar\\cyrp", + 0x0440: "\\cyrchar\\cyrr", + 0x0441: "\\cyrchar\\cyrs", + 0x0442: "\\cyrchar\\cyrt", + 0x0443: "\\cyrchar\\cyru", + 0x0444: "\\cyrchar\\cyrf", + 0x0445: "\\cyrchar\\cyrh", + 0x0446: "\\cyrchar\\cyrc", + 0x0447: "\\cyrchar\\cyrch", + 0x0448: "\\cyrchar\\cyrsh", + 0x0449: "\\cyrchar\\cyrshch", + 0x044A: "\\cyrchar\\cyrhrdsn", + 0x044B: "\\cyrchar\\cyrery", + 0x044C: "\\cyrchar\\cyrsftsn", + 0x044D: "\\cyrchar\\cyrerev", + 0x044E: "\\cyrchar\\cyryu", + 0x044F: "\\cyrchar\\cyrya", + 0x0451: "\\cyrchar\\cyryo", + 0x0452: "\\cyrchar\\cyrdje", + 0x0453: "\\cyrchar{\\'\\cyrg}", + 0x0454: "\\cyrchar\\cyrie", + 0x0455: "\\cyrchar\\cyrdze", + 0x0456: "\\cyrchar\\cyrii", + 0x0457: "\\cyrchar\\cyryi", + 0x0458: "\\cyrchar\\cyrje", + 0x0459: "\\cyrchar\\cyrlje", + 0x045A: "\\cyrchar\\cyrnje", + 0x045B: "\\cyrchar\\cyrtshe", + 0x045C: "\\cyrchar{\\'\\cyrk}", + 0x045E: "\\cyrchar\\cyrushrt", + 0x045F: "\\cyrchar\\cyrdzhe", + 0x0460: "\\cyrchar\\CYROMEGA", + 0x0461: "\\cyrchar\\cyromega", + 0x0462: "\\cyrchar\\CYRYAT", + 0x0464: "\\cyrchar\\CYRIOTE", + 0x0465: "\\cyrchar\\cyriote", + 0x0466: "\\cyrchar\\CYRLYUS", + 0x0467: "\\cyrchar\\cyrlyus", + 0x0468: "\\cyrchar\\CYRIOTLYUS", + 0x0469: "\\cyrchar\\cyriotlyus", + 0x046A: "\\cyrchar\\CYRBYUS", + 0x046C: "\\cyrchar\\CYRIOTBYUS", + 0x046D: "\\cyrchar\\cyriotbyus", + 0x046E: "\\cyrchar\\CYRKSI", + 0x046F: "\\cyrchar\\cyrksi", + 0x0470: "\\cyrchar\\CYRPSI", + 0x0471: "\\cyrchar\\cyrpsi", + 0x0472: "\\cyrchar\\CYRFITA", + 0x0474: "\\cyrchar\\CYRIZH", + 0x0478: "\\cyrchar\\CYRUK", + 0x0479: "\\cyrchar\\cyruk", + 0x047A: "\\cyrchar\\CYROMEGARND", + 0x047B: "\\cyrchar\\cyromegarnd", + 0x047C: "\\cyrchar\\CYROMEGATITLO", + 0x047D: "\\cyrchar\\cyromegatitlo", + 0x047E: "\\cyrchar\\CYROT", + 0x047F: "\\cyrchar\\cyrot", + 0x0480: "\\cyrchar\\CYRKOPPA", + 0x0481: "\\cyrchar\\cyrkoppa", + 0x0482: "\\cyrchar\\cyrthousands", + 0x0488: "\\cyrchar\\cyrhundredthousands", + 0x0489: "\\cyrchar\\cyrmillions", + 0x048C: "\\cyrchar\\CYRSEMISFTSN", + 0x048D: "\\cyrchar\\cyrsemisftsn", + 0x048E: "\\cyrchar\\CYRRTICK", + 0x048F: "\\cyrchar\\cyrrtick", + 0x0490: "\\cyrchar\\CYRGUP", + 0x0491: "\\cyrchar\\cyrgup", + 0x0492: "\\cyrchar\\CYRGHCRS", + 0x0493: "\\cyrchar\\cyrghcrs", + 0x0494: "\\cyrchar\\CYRGHK", + 0x0495: "\\cyrchar\\cyrghk", + 0x0496: "\\cyrchar\\CYRZHDSC", + 0x0497: "\\cyrchar\\cyrzhdsc", + 0x0498: "\\cyrchar\\CYRZDSC", + 0x0499: "\\cyrchar\\cyrzdsc", + 0x049A: "\\cyrchar\\CYRKDSC", + 0x049B: "\\cyrchar\\cyrkdsc", + 0x049C: "\\cyrchar\\CYRKVCRS", + 0x049D: "\\cyrchar\\cyrkvcrs", + 0x049E: "\\cyrchar\\CYRKHCRS", + 0x049F: "\\cyrchar\\cyrkhcrs", + 0x04A0: "\\cyrchar\\CYRKBEAK", + 0x04A1: "\\cyrchar\\cyrkbeak", + 0x04A2: "\\cyrchar\\CYRNDSC", + 0x04A3: "\\cyrchar\\cyrndsc", + 0x04A4: "\\cyrchar\\CYRNG", + 0x04A5: "\\cyrchar\\cyrng", + 0x04A6: "\\cyrchar\\CYRPHK", + 0x04A7: "\\cyrchar\\cyrphk", + 0x04A8: "\\cyrchar\\CYRABHHA", + 0x04A9: "\\cyrchar\\cyrabhha", + 0x04AA: "\\cyrchar\\CYRSDSC", + 0x04AB: "\\cyrchar\\cyrsdsc", + 0x04AC: "\\cyrchar\\CYRTDSC", + 0x04AD: "\\cyrchar\\cyrtdsc", + 0x04AE: "\\cyrchar\\CYRY", + 0x04AF: "\\cyrchar\\cyry", + 0x04B0: "\\cyrchar\\CYRYHCRS", + 0x04B1: "\\cyrchar\\cyryhcrs", + 0x04B2: "\\cyrchar\\CYRHDSC", + 0x04B3: "\\cyrchar\\cyrhdsc", + 0x04B4: "\\cyrchar\\CYRTETSE", + 0x04B5: "\\cyrchar\\cyrtetse", + 0x04B6: "\\cyrchar\\CYRCHRDSC", + 0x04B7: "\\cyrchar\\cyrchrdsc", + 0x04B8: "\\cyrchar\\CYRCHVCRS", + 0x04B9: "\\cyrchar\\cyrchvcrs", + 0x04BA: "\\cyrchar\\CYRSHHA", + 0x04BB: "\\cyrchar\\cyrshha", + 0x04BC: "\\cyrchar\\CYRABHCH", + 0x04BD: "\\cyrchar\\cyrabhch", + 0x04BE: "\\cyrchar\\CYRABHCHDSC", + 0x04BF: "\\cyrchar\\cyrabhchdsc", + 0x04C0: "\\cyrchar\\CYRpalochka", + 0x04C3: "\\cyrchar\\CYRKHK", + 0x04C4: "\\cyrchar\\cyrkhk", + 0x04C7: "\\cyrchar\\CYRNHK", + 0x04C8: "\\cyrchar\\cyrnhk", + 0x04CB: "\\cyrchar\\CYRCHLDSC", + 0x04CC: "\\cyrchar\\cyrchldsc", + 0x04D4: "\\cyrchar\\CYRAE", + 0x04D5: "\\cyrchar\\cyrae", + 0x04D8: "\\cyrchar\\CYRSCHWA", + 0x04D9: "\\cyrchar\\cyrschwa", + 0x04E0: "\\cyrchar\\CYRABHDZE", + 0x04E1: "\\cyrchar\\cyrabhdze", + 0x04E8: "\\cyrchar\\CYROTLD", + 0x04E9: "\\cyrchar\\cyrotld", + 0x2002: "\\hspace{0.6em}", + 0x2003: "\\hspace{1em}", + 0x2004: "\\hspace{0.33em}", + 0x2005: "\\hspace{0.25em}", + 0x2006: "\\hspace{0.166em}", + 0x2007: "\\hphantom{0}", + 0x2008: "\\hphantom{,}", + 0x2009: "\\hspace{0.167em}", + 0x200A: "\\mkern1mu ", + 0x2010: "-", + 0x2013: "\\textendash", + 0x2014: "\\textemdash", + 0x2015: "\\rule{1em}{1pt}", + 0x2016: "\\Vert", + 0x2018: "`", + 0x2019: "'", + 0x201A: ",", + 0x201C: "\\textquotedblleft", + 0x201D: "\\textquotedblright", + 0x201E: ",,", + 0x2020: "\\textdagger", + 0x2021: "\\textdaggerdbl", + 0x2022: "\\textbullet", + 0x2024: ".", + 0x2025: "..", + 0x2026: "\\ldots", + 0x2030: "\\textperthousand", + 0x2031: "\\textpertenthousand", + 0x2032: "{'}", + 0x2033: "{''}", + 0x2034: "{'''}", + 0x2035: "\\backprime", + 0x2039: "\\guilsinglleft", + 0x203A: "\\guilsinglright", + 0x2057: "''''", + 0x205F: "\\mkern4mu ", + 0x2060: "\\nolinebreak", + 0x20AC: "\\mbox{\\texteuro} ", + 0x20DB: "\\dddot", + 0x20DC: "\\ddddot", + 0x2102: "\\mathbb{C}", + 0x210A: "\\mathscr{g}", + 0x210B: "\\mathscr{H}", + 0x210C: "\\mathfrak{H}", + 0x210D: "\\mathbb{H}", + 0x210F: "\\hslash", + 0x2110: "\\mathscr{I}", + 0x2111: "\\mathfrak{I}", + 0x2112: "\\mathscr{L}", + 0x2113: "\\mathscr{l}", + 0x2115: "\\mathbb{N}", + 0x2116: "\\cyrchar\\textnumero", + 0x2118: "\\wp", + 0x2119: "\\mathbb{P}", + 0x211A: "\\mathbb{Q}", + 0x211B: "\\mathscr{R}", + 0x211C: "\\mathfrak{R}", + 0x211D: "\\mathbb{R}", + 0x2122: "\\texttrademark", + 0x2124: "\\mathbb{Z}", + 0x2126: "\\Omega", + 0x2127: "\\mho", + 0x2128: "\\mathfrak{Z}", + 0x212B: "\\AA", + 0x212C: "\\mathscr{B}", + 0x212D: "\\mathfrak{C}", + 0x212F: "\\mathscr{e}", + 0x2130: "\\mathscr{E}", + 0x2131: "\\mathscr{F}", + 0x2133: "\\mathscr{M}", + 0x2134: "\\mathscr{o}", + 0x2135: "\\aleph", + 0x2136: "\\beth", + 0x2137: "\\gimel", + 0x2138: "\\daleth", + 0x2153: "\\textfrac{1}{3}", + 0x2154: "\\textfrac{2}{3}", + 0x2155: "\\textfrac{1}{5}", + 0x2156: "\\textfrac{2}{5}", + 0x2157: "\\textfrac{3}{5}", + 0x2158: "\\textfrac{4}{5}", + 0x2159: "\\textfrac{1}{6}", + 0x215A: "\\textfrac{5}{6}", + 0x215B: "\\textfrac{1}{8}", + 0x215C: "\\textfrac{3}{8}", + 0x215D: "\\textfrac{5}{8}", + 0x215E: "\\textfrac{7}{8}", + 0x2190: "\\leftarrow", + 0x2191: "\\uparrow", + 0x2192: "\\rightarrow", + 0x2193: "\\downarrow", + 0x2194: "\\leftrightarrow", + 0x2195: "\\updownarrow", + 0x2196: "\\nwarrow", + 0x2197: "\\nearrow", + 0x2198: "\\searrow", + 0x2199: "\\swarrow", + 0x219A: "\\nleftarrow", + 0x219B: "\\nrightarrow", + 0x219C: "\\arrowwaveleft", + 0x219D: "\\arrowwaveright", + 0x219E: "\\twoheadleftarrow", + 0x21A0: "\\twoheadrightarrow", + 0x21A2: "\\leftarrowtail", + 0x21A3: "\\rightarrowtail", + 0x21A6: "\\mapsto", + 0x21A9: "\\hookleftarrow", + 0x21AA: "\\hookrightarrow", + 0x21AB: "\\looparrowleft", + 0x21AC: "\\looparrowright", + 0x21AD: "\\leftrightsquigarrow", + 0x21AE: "\\nleftrightarrow", + 0x21B0: "\\Lsh", + 0x21B1: "\\Rsh", + 0x21B6: "\\curvearrowleft", + 0x21B7: "\\curvearrowright", + 0x21BA: "\\circlearrowleft", + 0x21BB: "\\circlearrowright", + 0x21BC: "\\leftharpoonup", + 0x21BD: "\\leftharpoondown", + 0x21BE: "\\upharpoonright", + 0x21BF: "\\upharpoonleft", + 0x21C0: "\\rightharpoonup", + 0x21C1: "\\rightharpoondown", + 0x21C2: "\\downharpoonright", + 0x21C3: "\\downharpoonleft", + 0x21C4: "\\rightleftarrows", + 0x21C5: "\\dblarrowupdown", + 0x21C6: "\\leftrightarrows", + 0x21C7: "\\leftleftarrows", + 0x21C8: "\\upuparrows", + 0x21C9: "\\rightrightarrows", + 0x21CA: "\\downdownarrows", + 0x21CB: "\\leftrightharpoons", + 0x21CC: "\\rightleftharpoons", + 0x21CD: "\\nLeftarrow", + 0x21CE: "\\nLeftrightarrow", + 0x21CF: "\\nRightarrow", + 0x21D0: "\\Leftarrow", + 0x21D1: "\\Uparrow", + 0x21D2: "\\Rightarrow", + 0x21D3: "\\Downarrow", + 0x21D4: "\\Leftrightarrow", + 0x21D5: "\\Updownarrow", + 0x21DA: "\\Lleftarrow", + 0x21DB: "\\Rrightarrow", + 0x21DD: "\\rightsquigarrow", + 0x21F5: "\\DownArrowUpArrow", + 0x2200: "\\forall", + 0x2201: "\\complement", + 0x2202: "\\partial", + 0x2203: "\\exists", + 0x2204: "\\nexists", + 0x2205: "\\varnothing", + 0x2207: "\\nabla", + 0x2208: "\\in", + 0x2209: "\\not\\in", + 0x220B: "\\ni", + 0x220C: "\\not\\ni", + 0x220F: "\\prod", + 0x2210: "\\coprod", + 0x2211: "\\sum", + 0x2212: "-", + 0x2213: "\\mp", + 0x2214: "\\dotplus", + 0x2216: "\\setminus", + 0x2217: "{_\\ast}", + 0x2218: "\\circ", + 0x2219: "\\bullet", + 0x221A: "\\surd", + 0x221D: "\\propto", + 0x221E: "\\infty", + 0x221F: "\\rightangle", + 0x2220: "\\angle", + 0x2221: "\\measuredangle", + 0x2222: "\\sphericalangle", + 0x2223: "\\mid", + 0x2224: "\\nmid", + 0x2225: "\\parallel", + 0x2226: "\\nparallel", + 0x2227: "\\wedge", + 0x2228: "\\vee", + 0x2229: "\\cap", + 0x222A: "\\cup", + 0x222B: "\\int", + 0x222C: "\\int\\!\\int", + 0x222D: "\\int\\!\\int\\!\\int", + 0x222E: "\\oint", + 0x222F: "\\surfintegral", + 0x2230: "\\volintegral", + 0x2231: "\\clwintegral", + 0x2234: "\\therefore", + 0x2235: "\\because", + 0x2237: "\\Colon", + 0x223A: "\\mathbin{{:}\\!\\!{-}\\!\\!{:}}", + 0x223B: "\\homothetic", + 0x223C: "\\sim", + 0x223D: "\\backsim", + 0x223E: "\\lazysinv", + 0x2240: "\\wr", + 0x2241: "\\not\\sim", + 0x2243: "\\simeq", + 0x2244: "\\not\\simeq", + 0x2245: "\\cong", + 0x2246: "\\approxnotequal", + 0x2247: "\\not\\cong", + 0x2248: "\\approx", + 0x2249: "\\not\\approx", + 0x224A: "\\approxeq", + 0x224B: "\\tildetrpl", + 0x224C: "\\allequal", + 0x224D: "\\asymp", + 0x224E: "\\Bumpeq", + 0x224F: "\\bumpeq", + 0x2250: "\\doteq", + 0x2251: "\\doteqdot", + 0x2252: "\\fallingdotseq", + 0x2253: "\\risingdotseq", + 0x2254: ":=", + 0x2255: "=:", + 0x2256: "\\eqcirc", + 0x2257: "\\circeq", + 0x2259: "\\estimates", + 0x225B: "\\starequal", + 0x225C: "\\triangleq", + 0x2260: "\\not =", + 0x2261: "\\equiv", + 0x2262: "\\not\\equiv", + 0x2264: "\\leq", + 0x2265: "\\geq", + 0x2266: "\\leqq", + 0x2267: "\\geqq", + 0x2268: "\\lneqq", + 0x2269: "\\gneqq", + 0x226A: "\\ll", + 0x226B: "\\gg", + 0x226C: "\\between", + 0x226D: "\\not\\kern-0.3em\\times", + 0x226E: "\\not<", + 0x226F: "\\not>", + 0x2270: "\\not\\leq", + 0x2271: "\\not\\geq", + 0x2272: "\\lessequivlnt", + 0x2273: "\\greaterequivlnt", + 0x2276: "\\lessgtr", + 0x2277: "\\gtrless", + 0x2278: "\\notlessgreater", + 0x2279: "\\notgreaterless", + 0x227A: "\\prec", + 0x227B: "\\succ", + 0x227C: "\\preccurlyeq", + 0x227D: "\\succcurlyeq", + 0x227E: "\\precapprox", + 0x227F: "\\succapprox", + 0x2280: "\\not\\prec", + 0x2281: "\\not\\succ", + 0x2282: "\\subset", + 0x2283: "\\supset", + 0x2284: "\\not\\subset", + 0x2285: "\\not\\supset", + 0x2286: "\\subseteq", + 0x2287: "\\supseteq", + 0x2288: "\\not\\subseteq", + 0x2289: "\\not\\supseteq", + 0x228A: "\\subsetneq", + 0x228B: "\\supsetneq", + 0x228E: "\\uplus", + 0x228F: "\\sqsubset", + 0x2290: "\\sqsupset", + 0x2291: "\\sqsubseteq", + 0x2292: "\\sqsupseteq", + 0x2293: "\\sqcap", + 0x2294: "\\sqcup", + 0x2295: "\\oplus", + 0x2296: "\\ominus", + 0x2297: "\\otimes", + 0x2298: "\\oslash", + 0x2299: "\\odot", + 0x229A: "\\circledcirc", + 0x229B: "\\circledast", + 0x229D: "\\circleddash", + 0x229E: "\\boxplus", + 0x229F: "\\boxminus", + 0x22A0: "\\boxtimes", + 0x22A1: "\\boxdot", + 0x22A2: "\\vdash", + 0x22A3: "\\dashv", + 0x22A4: "\\top", + 0x22A5: "\\perp", + 0x22A7: "\\truestate", + 0x22A8: "\\forcesextra", + 0x22A9: "\\Vdash", + 0x22AA: "\\Vvdash", + 0x22AB: "\\VDash", + 0x22AC: "\\nvdash", + 0x22AD: "\\nvDash", + 0x22AE: "\\nVdash", + 0x22AF: "\\nVDash", + 0x22B2: "\\vartriangleleft", + 0x22B3: "\\vartriangleright", + 0x22B4: "\\trianglelefteq", + 0x22B5: "\\trianglerighteq", + 0x22B6: "\\original", + 0x22B7: "\\image", + 0x22B8: "\\multimap", + 0x22B9: "\\hermitconjmatrix", + 0x22BA: "\\intercal", + 0x22BB: "\\veebar", + 0x22BE: "\\rightanglearc", + 0x22C2: "\\bigcap", + 0x22C3: "\\bigcup", + 0x22C4: "\\diamond", + 0x22C5: "\\cdot", + 0x22C6: "\\star", + 0x22C7: "\\divideontimes", + 0x22C8: "\\bowtie", + 0x22C9: "\\ltimes", + 0x22CA: "\\rtimes", + 0x22CB: "\\leftthreetimes", + 0x22CC: "\\rightthreetimes", + 0x22CD: "\\backsimeq", + 0x22CE: "\\curlyvee", + 0x22CF: "\\curlywedge", + 0x22D0: "\\Subset", + 0x22D1: "\\Supset", + 0x22D2: "\\Cap", + 0x22D3: "\\Cup", + 0x22D4: "\\pitchfork", + 0x22D6: "\\lessdot", + 0x22D7: "\\gtrdot", + 0x22D8: "\\verymuchless", + 0x22D9: "\\verymuchgreater", + 0x22DA: "\\lesseqgtr", + 0x22DB: "\\gtreqless", + 0x22DE: "\\curlyeqprec", + 0x22DF: "\\curlyeqsucc", + 0x22E2: "\\not\\sqsubseteq", + 0x22E3: "\\not\\sqsupseteq", + 0x22E6: "\\lnsim", + 0x22E7: "\\gnsim", + 0x22E8: "\\precedesnotsimilar", + 0x22E9: "\\succnsim", + 0x22EA: "\\ntriangleleft", + 0x22EB: "\\ntriangleright", + 0x22EC: "\\ntrianglelefteq", + 0x22ED: "\\ntrianglerighteq", + 0x22EE: "\\vdots", + 0x22EF: "\\cdots", + 0x22F0: "\\upslopeellipsis", + 0x22F1: "\\downslopeellipsis", + 0x2305: "\\barwedge", + 0x2306: "\\varperspcorrespond", + 0x2308: "\\lceil", + 0x2309: "\\rceil", + 0x230A: "\\lfloor", + 0x230B: "\\rfloor", + 0x2315: "\\recorder", + 0x2316: '\\mathchar"2208', + 0x231C: "\\ulcorner", + 0x231D: "\\urcorner", + 0x231E: "\\llcorner", + 0x231F: "\\lrcorner", + 0x2322: "\\frown", + 0x2323: "\\smile", + 0x23B0: "\\lmoustache", + 0x23B1: "\\rmoustache", + 0x2423: "\\textvisiblespace", + 0x2460: "\\ding{172}", + 0x2461: "\\ding{173}", + 0x2462: "\\ding{174}", + 0x2463: "\\ding{175}", + 0x2464: "\\ding{176}", + 0x2465: "\\ding{177}", + 0x2466: "\\ding{178}", + 0x2467: "\\ding{179}", + 0x2468: "\\ding{180}", + 0x2469: "\\ding{181}", + 0x24C8: "\\circledS", + 0x2571: "\\diagup", + 0x25A0: "\\ding{110}", + 0x25A1: "\\square", + 0x25AA: "\\blacksquare", + 0x25AD: "\\fbox{~~}", + 0x25B2: "\\ding{115}", + 0x25B3: "\\bigtriangleup", + 0x25B4: "\\blacktriangle", + 0x25B5: "\\vartriangle", + 0x25B8: "\\blacktriangleright", + 0x25B9: "\\triangleright", + 0x25BC: "\\ding{116}", + 0x25BD: "\\bigtriangledown", + 0x25BE: "\\blacktriangledown", + 0x25BF: "\\triangledown", + 0x25C2: "\\blacktriangleleft", + 0x25C3: "\\triangleleft", + 0x25C6: "\\ding{117}", + 0x25CA: "\\lozenge", + 0x25CB: "\\bigcirc", + 0x25CF: "\\ding{108}", + 0x25D7: "\\ding{119}", + 0x25EF: "\\bigcirc", + 0x2605: "\\ding{72}", + 0x2606: "\\ding{73}", + 0x260E: "\\ding{37}", + 0x261B: "\\ding{42}", + 0x261E: "\\ding{43}", + 0x263E: "\\rightmoon", + 0x263F: "\\mercury", + 0x2640: "\\venus", + 0x2642: "\\male", + 0x2643: "\\jupiter", + 0x2644: "\\saturn", + 0x2645: "\\uranus", + 0x2646: "\\neptune", + 0x2647: "\\pluto", + 0x2648: "\\aries", + 0x2649: "\\taurus", + 0x264A: "\\gemini", + 0x264B: "\\cancer", + 0x264C: "\\leo", + 0x264D: "\\virgo", + 0x264E: "\\libra", + 0x264F: "\\scorpio", + 0x2650: "\\sagittarius", + 0x2651: "\\capricornus", + 0x2652: "\\aquarius", + 0x2653: "\\pisces", + 0x2660: "\\ding{171}", + 0x2662: "\\diamond", + 0x2663: "\\ding{168}", + 0x2665: "\\ding{170}", + 0x2666: "\\ding{169}", + 0x2669: "\\quarternote", + 0x266A: "\\eighthnote", + 0x266D: "\\flat", + 0x266E: "\\natural", + 0x266F: "\\sharp", + 0x2701: "\\ding{33}", + 0x2702: "\\ding{34}", + 0x2703: "\\ding{35}", + 0x2704: "\\ding{36}", + 0x2706: "\\ding{38}", + 0x2707: "\\ding{39}", + 0x2708: "\\ding{40}", + 0x2709: "\\ding{41}", + 0x270C: "\\ding{44}", + 0x270D: "\\ding{45}", + 0x270E: "\\ding{46}", + 0x270F: "\\ding{47}", + 0x2710: "\\ding{48}", + 0x2711: "\\ding{49}", + 0x2712: "\\ding{50}", + 0x2713: "\\ding{51}", + 0x2714: "\\ding{52}", + 0x2715: "\\ding{53}", + 0x2716: "\\ding{54}", + 0x2717: "\\ding{55}", + 0x2718: "\\ding{56}", + 0x2719: "\\ding{57}", + 0x271A: "\\ding{58}", + 0x271B: "\\ding{59}", + 0x271C: "\\ding{60}", + 0x271D: "\\ding{61}", + 0x271E: "\\ding{62}", + 0x271F: "\\ding{63}", + 0x2720: "\\ding{64}", + 0x2721: "\\ding{65}", + 0x2722: "\\ding{66}", + 0x2723: "\\ding{67}", + 0x2724: "\\ding{68}", + 0x2725: "\\ding{69}", + 0x2726: "\\ding{70}", + 0x2727: "\\ding{71}", + 0x2729: "\\ding{73}", + 0x272A: "\\ding{74}", + 0x272B: "\\ding{75}", + 0x272C: "\\ding{76}", + 0x272D: "\\ding{77}", + 0x272E: "\\ding{78}", + 0x272F: "\\ding{79}", + 0x2730: "\\ding{80}", + 0x2731: "\\ding{81}", + 0x2732: "\\ding{82}", + 0x2733: "\\ding{83}", + 0x2734: "\\ding{84}", + 0x2735: "\\ding{85}", + 0x2736: "\\ding{86}", + 0x2737: "\\ding{87}", + 0x2738: "\\ding{88}", + 0x2739: "\\ding{89}", + 0x273A: "\\ding{90}", + 0x273B: "\\ding{91}", + 0x273C: "\\ding{92}", + 0x273D: "\\ding{93}", + 0x273E: "\\ding{94}", + 0x273F: "\\ding{95}", + 0x2740: "\\ding{96}", + 0x2741: "\\ding{97}", + 0x2742: "\\ding{98}", + 0x2743: "\\ding{99}", + 0x2744: "\\ding{100}", + 0x2745: "\\ding{101}", + 0x2746: "\\ding{102}", + 0x2747: "\\ding{103}", + 0x2748: "\\ding{104}", + 0x2749: "\\ding{105}", + 0x274A: "\\ding{106}", + 0x274B: "\\ding{107}", + 0x274D: "\\ding{109}", + 0x274F: "\\ding{111}", + 0x2750: "\\ding{112}", + 0x2751: "\\ding{113}", + 0x2752: "\\ding{114}", + 0x2756: "\\ding{118}", + 0x2758: "\\ding{120}", + 0x2759: "\\ding{121}", + 0x275A: "\\ding{122}", + 0x275B: "\\ding{123}", + 0x275C: "\\ding{124}", + 0x275D: "\\ding{125}", + 0x275E: "\\ding{126}", + 0x2761: "\\ding{161}", + 0x2762: "\\ding{162}", + 0x2763: "\\ding{163}", + 0x2764: "\\ding{164}", + 0x2765: "\\ding{165}", + 0x2766: "\\ding{166}", + 0x2767: "\\ding{167}", + 0x2776: "\\ding{182}", + 0x2777: "\\ding{183}", + 0x2778: "\\ding{184}", + 0x2779: "\\ding{185}", + 0x277A: "\\ding{186}", + 0x277B: "\\ding{187}", + 0x277C: "\\ding{188}", + 0x277D: "\\ding{189}", + 0x277E: "\\ding{190}", + 0x277F: "\\ding{191}", + 0x2780: "\\ding{192}", + 0x2781: "\\ding{193}", + 0x2782: "\\ding{194}", + 0x2783: "\\ding{195}", + 0x2784: "\\ding{196}", + 0x2785: "\\ding{197}", + 0x2786: "\\ding{198}", + 0x2787: "\\ding{199}", + 0x2788: "\\ding{200}", + 0x2789: "\\ding{201}", + 0x278A: "\\ding{202}", + 0x278B: "\\ding{203}", + 0x278C: "\\ding{204}", + 0x278D: "\\ding{205}", + 0x278E: "\\ding{206}", + 0x278F: "\\ding{207}", + 0x2790: "\\ding{208}", + 0x2791: "\\ding{209}", + 0x2792: "\\ding{210}", + 0x2793: "\\ding{211}", + 0x2794: "\\ding{212}", + 0x2798: "\\ding{216}", + 0x2799: "\\ding{217}", + 0x279A: "\\ding{218}", + 0x279B: "\\ding{219}", + 0x279C: "\\ding{220}", + 0x279D: "\\ding{221}", + 0x279E: "\\ding{222}", + 0x279F: "\\ding{223}", + 0x27A0: "\\ding{224}", + 0x27A1: "\\ding{225}", + 0x27A2: "\\ding{226}", + 0x27A3: "\\ding{227}", + 0x27A4: "\\ding{228}", + 0x27A5: "\\ding{229}", + 0x27A6: "\\ding{230}", + 0x27A7: "\\ding{231}", + 0x27A8: "\\ding{232}", + 0x27A9: "\\ding{233}", + 0x27AA: "\\ding{234}", + 0x27AB: "\\ding{235}", + 0x27AC: "\\ding{236}", + 0x27AD: "\\ding{237}", + 0x27AE: "\\ding{238}", + 0x27AF: "\\ding{239}", + 0x27B1: "\\ding{241}", + 0x27B2: "\\ding{242}", + 0x27B3: "\\ding{243}", + 0x27B4: "\\ding{244}", + 0x27B5: "\\ding{245}", + 0x27B6: "\\ding{246}", + 0x27B7: "\\ding{247}", + 0x27B8: "\\ding{248}", + 0x27B9: "\\ding{249}", + 0x27BA: "\\ding{250}", + 0x27BB: "\\ding{251}", + 0x27BC: "\\ding{252}", + 0x27BD: "\\ding{253}", + 0x27BE: "\\ding{254}", + 0x27E8: "\\langle", + 0x27E9: "\\rangle", + 0x27F5: "\\longleftarrow", + 0x27F6: "\\longrightarrow", + 0x27F7: "\\longleftrightarrow", + 0x27F8: "\\Longleftarrow", + 0x27F9: "\\Longrightarrow", + 0x27FA: "\\Longleftrightarrow", + 0x27FC: "\\longmapsto", + 0x27FF: "\\sim\\joinrel\\leadsto", + 0x2912: "\\UpArrowBar", + 0x2913: "\\DownArrowBar", + 0x294E: "\\LeftRightVector", + 0x294F: "\\RightUpDownVector", + 0x2950: "\\DownLeftRightVector", + 0x2951: "\\LeftUpDownVector", + 0x2952: "\\LeftVectorBar", + 0x2953: "\\RightVectorBar", + 0x2954: "\\RightUpVectorBar", + 0x2955: "\\RightDownVectorBar", + 0x2956: "\\DownLeftVectorBar", + 0x2957: "\\DownRightVectorBar", + 0x2958: "\\LeftUpVectorBar", + 0x2959: "\\LeftDownVectorBar", + 0x295A: "\\LeftTeeVector", + 0x295B: "\\RightTeeVector", + 0x295C: "\\RightUpTeeVector", + 0x295D: "\\RightDownTeeVector", + 0x295E: "\\DownLeftTeeVector", + 0x295F: "\\DownRightTeeVector", + 0x2960: "\\LeftUpTeeVector", + 0x2961: "\\LeftDownTeeVector", + 0x296E: "\\UpEquilibrium", + 0x296F: "\\ReverseUpEquilibrium", + 0x2970: "\\RoundImplies", + 0x2993: "<\\kern-0.58em(", + 0x299C: "\\Angle", + 0x29CF: "\\LeftTriangleBar", + 0x29D0: "\\RightTriangleBar", + 0x29EB: "\\blacklozenge", + 0x29F4: "\\RuleDelayed", + 0x2A0F: "\\clockoint", + 0x2A16: "\\sqrint", + 0x2A3F: "\\amalg", + 0x2A5E: "\\perspcorrespond", + 0x2A6E: "\\stackrel{*}{=}", + 0x2A75: "\\Equal", + 0x2A7D: "\\leqslant", + 0x2A7E: "\\geqslant", + 0x2A85: "\\lessapprox", + 0x2A86: "\\gtrapprox", + 0x2A87: "\\lneq", + 0x2A88: "\\gneq", + 0x2A89: "\\lnapprox", + 0x2A8A: "\\gnapprox", + 0x2A8B: "\\lesseqqgtr", + 0x2A8C: "\\gtreqqless", + 0x2A95: "\\eqslantless", + 0x2A96: "\\eqslantgtr", + 0x2A9D: "\\Pisymbol{ppi020}{117}", + 0x2A9E: "\\Pisymbol{ppi020}{105}", + 0x2AA1: "\\NestedLessLess", + 0x2AA2: "\\NestedGreaterGreater", + 0x2AAF: "\\preceq", + 0x2AB0: "\\succeq", + 0x2AB5: "\\precneqq", + 0x2AB6: "\\succneqq", + 0x2AB7: "\\precapprox", + 0x2AB8: "\\succapprox", + 0x2AB9: "\\precnapprox", + 0x2ABA: "\\succnapprox", + 0x2AC5: "\\subseteqq", + 0x2AC6: "\\supseteqq", + 0x2ACB: "\\subsetneqq", + 0x2ACC: "\\supsetneqq", + 0x2AFD: "{{/}\\!\\!{/}}", + 0x301A: "\\openbracketleft", + 0x301B: "\\openbracketright", + 0xFB00: "ff", + 0xFB01: "fi", + 0xFB02: "fl", + 0xFB03: "ffi", + 0xFB04: "ffl", + 0x1D400: "\\mathbf{A}", + 0x1D401: "\\mathbf{B}", + 0x1D402: "\\mathbf{C}", + 0x1D403: "\\mathbf{D}", + 0x1D404: "\\mathbf{E}", + 0x1D405: "\\mathbf{F}", + 0x1D406: "\\mathbf{G}", + 0x1D407: "\\mathbf{H}", + 0x1D408: "\\mathbf{I}", + 0x1D409: "\\mathbf{J}", + 0x1D40A: "\\mathbf{K}", + 0x1D40B: "\\mathbf{L}", + 0x1D40C: "\\mathbf{M}", + 0x1D40D: "\\mathbf{N}", + 0x1D40E: "\\mathbf{O}", + 0x1D40F: "\\mathbf{P}", + 0x1D410: "\\mathbf{Q}", + 0x1D411: "\\mathbf{R}", + 0x1D412: "\\mathbf{S}", + 0x1D413: "\\mathbf{T}", + 0x1D414: "\\mathbf{U}", + 0x1D415: "\\mathbf{V}", + 0x1D416: "\\mathbf{W}", + 0x1D417: "\\mathbf{X}", + 0x1D418: "\\mathbf{Y}", + 0x1D419: "\\mathbf{Z}", + 0x1D41A: "\\mathbf{a}", + 0x1D41B: "\\mathbf{b}", + 0x1D41C: "\\mathbf{c}", + 0x1D41D: "\\mathbf{d}", + 0x1D41E: "\\mathbf{e}", + 0x1D41F: "\\mathbf{f}", + 0x1D420: "\\mathbf{g}", + 0x1D421: "\\mathbf{h}", + 0x1D422: "\\mathbf{i}", + 0x1D423: "\\mathbf{j}", + 0x1D424: "\\mathbf{k}", + 0x1D425: "\\mathbf{l}", + 0x1D426: "\\mathbf{m}", + 0x1D427: "\\mathbf{n}", + 0x1D428: "\\mathbf{o}", + 0x1D429: "\\mathbf{p}", + 0x1D42A: "\\mathbf{q}", + 0x1D42B: "\\mathbf{r}", + 0x1D42C: "\\mathbf{s}", + 0x1D42D: "\\mathbf{t}", + 0x1D42E: "\\mathbf{u}", + 0x1D42F: "\\mathbf{v}", + 0x1D430: "\\mathbf{w}", + 0x1D431: "\\mathbf{x}", + 0x1D432: "\\mathbf{y}", + 0x1D433: "\\mathbf{z}", + 0x1D434: "\\mathmit{A}", + 0x1D435: "\\mathmit{B}", + 0x1D436: "\\mathmit{C}", + 0x1D437: "\\mathmit{D}", + 0x1D438: "\\mathmit{E}", + 0x1D439: "\\mathmit{F}", + 0x1D43A: "\\mathmit{G}", + 0x1D43B: "\\mathmit{H}", + 0x1D43C: "\\mathmit{I}", + 0x1D43D: "\\mathmit{J}", + 0x1D43E: "\\mathmit{K}", + 0x1D43F: "\\mathmit{L}", + 0x1D440: "\\mathmit{M}", + 0x1D441: "\\mathmit{N}", + 0x1D442: "\\mathmit{O}", + 0x1D443: "\\mathmit{P}", + 0x1D444: "\\mathmit{Q}", + 0x1D445: "\\mathmit{R}", + 0x1D446: "\\mathmit{S}", + 0x1D447: "\\mathmit{T}", + 0x1D448: "\\mathmit{U}", + 0x1D449: "\\mathmit{V}", + 0x1D44A: "\\mathmit{W}", + 0x1D44B: "\\mathmit{X}", + 0x1D44C: "\\mathmit{Y}", + 0x1D44D: "\\mathmit{Z}", + 0x1D44E: "\\mathmit{a}", + 0x1D44F: "\\mathmit{b}", + 0x1D450: "\\mathmit{c}", + 0x1D451: "\\mathmit{d}", + 0x1D452: "\\mathmit{e}", + 0x1D453: "\\mathmit{f}", + 0x1D454: "\\mathmit{g}", + 0x1D456: "\\mathmit{i}", + 0x1D457: "\\mathmit{j}", + 0x1D458: "\\mathmit{k}", + 0x1D459: "\\mathmit{l}", + 0x1D45A: "\\mathmit{m}", + 0x1D45B: "\\mathmit{n}", + 0x1D45C: "\\mathmit{o}", + 0x1D45D: "\\mathmit{p}", + 0x1D45E: "\\mathmit{q}", + 0x1D45F: "\\mathmit{r}", + 0x1D460: "\\mathmit{s}", + 0x1D461: "\\mathmit{t}", + 0x1D462: "\\mathmit{u}", + 0x1D463: "\\mathmit{v}", + 0x1D464: "\\mathmit{w}", + 0x1D465: "\\mathmit{x}", + 0x1D466: "\\mathmit{y}", + 0x1D467: "\\mathmit{z}", + 0x1D468: "\\mathbit{A}", + 0x1D469: "\\mathbit{B}", + 0x1D46A: "\\mathbit{C}", + 0x1D46B: "\\mathbit{D}", + 0x1D46C: "\\mathbit{E}", + 0x1D46D: "\\mathbit{F}", + 0x1D46E: "\\mathbit{G}", + 0x1D46F: "\\mathbit{H}", + 0x1D470: "\\mathbit{I}", + 0x1D471: "\\mathbit{J}", + 0x1D472: "\\mathbit{K}", + 0x1D473: "\\mathbit{L}", + 0x1D474: "\\mathbit{M}", + 0x1D475: "\\mathbit{N}", + 0x1D476: "\\mathbit{O}", + 0x1D477: "\\mathbit{P}", + 0x1D478: "\\mathbit{Q}", + 0x1D479: "\\mathbit{R}", + 0x1D47A: "\\mathbit{S}", + 0x1D47B: "\\mathbit{T}", + 0x1D47C: "\\mathbit{U}", + 0x1D47D: "\\mathbit{V}", + 0x1D47E: "\\mathbit{W}", + 0x1D47F: "\\mathbit{X}", + 0x1D480: "\\mathbit{Y}", + 0x1D481: "\\mathbit{Z}", + 0x1D482: "\\mathbit{a}", + 0x1D483: "\\mathbit{b}", + 0x1D484: "\\mathbit{c}", + 0x1D485: "\\mathbit{d}", + 0x1D486: "\\mathbit{e}", + 0x1D487: "\\mathbit{f}", + 0x1D488: "\\mathbit{g}", + 0x1D489: "\\mathbit{h}", + 0x1D48A: "\\mathbit{i}", + 0x1D48B: "\\mathbit{j}", + 0x1D48C: "\\mathbit{k}", + 0x1D48D: "\\mathbit{l}", + 0x1D48E: "\\mathbit{m}", + 0x1D48F: "\\mathbit{n}", + 0x1D490: "\\mathbit{o}", + 0x1D491: "\\mathbit{p}", + 0x1D492: "\\mathbit{q}", + 0x1D493: "\\mathbit{r}", + 0x1D494: "\\mathbit{s}", + 0x1D495: "\\mathbit{t}", + 0x1D496: "\\mathbit{u}", + 0x1D497: "\\mathbit{v}", + 0x1D498: "\\mathbit{w}", + 0x1D499: "\\mathbit{x}", + 0x1D49A: "\\mathbit{y}", + 0x1D49B: "\\mathbit{z}", + 0x1D49C: "\\mathscr{A}", + 0x1D49E: "\\mathscr{C}", + 0x1D49F: "\\mathscr{D}", + 0x1D4A2: "\\mathscr{G}", + 0x1D4A5: "\\mathscr{J}", + 0x1D4A6: "\\mathscr{K}", + 0x1D4A9: "\\mathscr{N}", + 0x1D4AA: "\\mathscr{O}", + 0x1D4AB: "\\mathscr{P}", + 0x1D4AC: "\\mathscr{Q}", + 0x1D4AE: "\\mathscr{S}", + 0x1D4AF: "\\mathscr{T}", + 0x1D4B0: "\\mathscr{U}", + 0x1D4B1: "\\mathscr{V}", + 0x1D4B2: "\\mathscr{W}", + 0x1D4B3: "\\mathscr{X}", + 0x1D4B4: "\\mathscr{Y}", + 0x1D4B5: "\\mathscr{Z}", + 0x1D4B6: "\\mathscr{a}", + 0x1D4B7: "\\mathscr{b}", + 0x1D4B8: "\\mathscr{c}", + 0x1D4B9: "\\mathscr{d}", + 0x1D4BB: "\\mathscr{f}", + 0x1D4BD: "\\mathscr{h}", + 0x1D4BE: "\\mathscr{i}", + 0x1D4BF: "\\mathscr{j}", + 0x1D4C0: "\\mathscr{k}", + 0x1D4C1: "\\mathscr{l}", + 0x1D4C2: "\\mathscr{m}", + 0x1D4C3: "\\mathscr{n}", + 0x1D4C5: "\\mathscr{p}", + 0x1D4C6: "\\mathscr{q}", + 0x1D4C7: "\\mathscr{r}", + 0x1D4C8: "\\mathscr{s}", + 0x1D4C9: "\\mathscr{t}", + 0x1D4CA: "\\mathscr{u}", + 0x1D4CB: "\\mathscr{v}", + 0x1D4CC: "\\mathscr{w}", + 0x1D4CD: "\\mathscr{x}", + 0x1D4CE: "\\mathscr{y}", + 0x1D4CF: "\\mathscr{z}", + 0x1D4D0: "\\mathbcal{A}", + 0x1D4D1: "\\mathbcal{B}", + 0x1D4D2: "\\mathbcal{C}", + 0x1D4D3: "\\mathbcal{D}", + 0x1D4D4: "\\mathbcal{E}", + 0x1D4D5: "\\mathbcal{F}", + 0x1D4D6: "\\mathbcal{G}", + 0x1D4D7: "\\mathbcal{H}", + 0x1D4D8: "\\mathbcal{I}", + 0x1D4D9: "\\mathbcal{J}", + 0x1D4DA: "\\mathbcal{K}", + 0x1D4DB: "\\mathbcal{L}", + 0x1D4DC: "\\mathbcal{M}", + 0x1D4DD: "\\mathbcal{N}", + 0x1D4DE: "\\mathbcal{O}", + 0x1D4DF: "\\mathbcal{P}", + 0x1D4E0: "\\mathbcal{Q}", + 0x1D4E1: "\\mathbcal{R}", + 0x1D4E2: "\\mathbcal{S}", + 0x1D4E3: "\\mathbcal{T}", + 0x1D4E4: "\\mathbcal{U}", + 0x1D4E5: "\\mathbcal{V}", + 0x1D4E6: "\\mathbcal{W}", + 0x1D4E7: "\\mathbcal{X}", + 0x1D4E8: "\\mathbcal{Y}", + 0x1D4E9: "\\mathbcal{Z}", + 0x1D4EA: "\\mathbcal{a}", + 0x1D4EB: "\\mathbcal{b}", + 0x1D4EC: "\\mathbcal{c}", + 0x1D4ED: "\\mathbcal{d}", + 0x1D4EE: "\\mathbcal{e}", + 0x1D4EF: "\\mathbcal{f}", + 0x1D4F0: "\\mathbcal{g}", + 0x1D4F1: "\\mathbcal{h}", + 0x1D4F2: "\\mathbcal{i}", + 0x1D4F3: "\\mathbcal{j}", + 0x1D4F4: "\\mathbcal{k}", + 0x1D4F5: "\\mathbcal{l}", + 0x1D4F6: "\\mathbcal{m}", + 0x1D4F7: "\\mathbcal{n}", + 0x1D4F8: "\\mathbcal{o}", + 0x1D4F9: "\\mathbcal{p}", + 0x1D4FA: "\\mathbcal{q}", + 0x1D4FB: "\\mathbcal{r}", + 0x1D4FC: "\\mathbcal{s}", + 0x1D4FD: "\\mathbcal{t}", + 0x1D4FE: "\\mathbcal{u}", + 0x1D4FF: "\\mathbcal{v}", + 0x1D500: "\\mathbcal{w}", + 0x1D501: "\\mathbcal{x}", + 0x1D502: "\\mathbcal{y}", + 0x1D503: "\\mathbcal{z}", + 0x1D504: "\\mathfrak{A}", + 0x1D505: "\\mathfrak{B}", + 0x1D507: "\\mathfrak{D}", + 0x1D508: "\\mathfrak{E}", + 0x1D509: "\\mathfrak{F}", + 0x1D50A: "\\mathfrak{G}", + 0x1D50D: "\\mathfrak{J}", + 0x1D50E: "\\mathfrak{K}", + 0x1D50F: "\\mathfrak{L}", + 0x1D510: "\\mathfrak{M}", + 0x1D511: "\\mathfrak{N}", + 0x1D512: "\\mathfrak{O}", + 0x1D513: "\\mathfrak{P}", + 0x1D514: "\\mathfrak{Q}", + 0x1D516: "\\mathfrak{S}", + 0x1D517: "\\mathfrak{T}", + 0x1D518: "\\mathfrak{U}", + 0x1D519: "\\mathfrak{V}", + 0x1D51A: "\\mathfrak{W}", + 0x1D51B: "\\mathfrak{X}", + 0x1D51C: "\\mathfrak{Y}", + 0x1D51E: "\\mathfrak{a}", + 0x1D51F: "\\mathfrak{b}", + 0x1D520: "\\mathfrak{c}", + 0x1D521: "\\mathfrak{d}", + 0x1D522: "\\mathfrak{e}", + 0x1D523: "\\mathfrak{f}", + 0x1D524: "\\mathfrak{g}", + 0x1D525: "\\mathfrak{h}", + 0x1D526: "\\mathfrak{i}", + 0x1D527: "\\mathfrak{j}", + 0x1D528: "\\mathfrak{k}", + 0x1D529: "\\mathfrak{l}", + 0x1D52A: "\\mathfrak{m}", + 0x1D52B: "\\mathfrak{n}", + 0x1D52C: "\\mathfrak{o}", + 0x1D52D: "\\mathfrak{p}", + 0x1D52E: "\\mathfrak{q}", + 0x1D52F: "\\mathfrak{r}", + 0x1D530: "\\mathfrak{s}", + 0x1D531: "\\mathfrak{t}", + 0x1D532: "\\mathfrak{u}", + 0x1D533: "\\mathfrak{v}", + 0x1D534: "\\mathfrak{w}", + 0x1D535: "\\mathfrak{x}", + 0x1D536: "\\mathfrak{y}", + 0x1D537: "\\mathfrak{z}", + 0x1D538: "\\mathbb{A}", + 0x1D539: "\\mathbb{B}", + 0x1D53B: "\\mathbb{D}", + 0x1D53C: "\\mathbb{E}", + 0x1D53D: "\\mathbb{F}", + 0x1D53E: "\\mathbb{G}", + 0x1D540: "\\mathbb{I}", + 0x1D541: "\\mathbb{J}", + 0x1D542: "\\mathbb{K}", + 0x1D543: "\\mathbb{L}", + 0x1D544: "\\mathbb{M}", + 0x1D546: "\\mathbb{O}", + 0x1D54A: "\\mathbb{S}", + 0x1D54B: "\\mathbb{T}", + 0x1D54C: "\\mathbb{U}", + 0x1D54D: "\\mathbb{V}", + 0x1D54E: "\\mathbb{W}", + 0x1D54F: "\\mathbb{X}", + 0x1D550: "\\mathbb{Y}", + 0x1D552: "\\mathbb{a}", + 0x1D553: "\\mathbb{b}", + 0x1D554: "\\mathbb{c}", + 0x1D555: "\\mathbb{d}", + 0x1D556: "\\mathbb{e}", + 0x1D557: "\\mathbb{f}", + 0x1D558: "\\mathbb{g}", + 0x1D559: "\\mathbb{h}", + 0x1D55A: "\\mathbb{i}", + 0x1D55B: "\\mathbb{j}", + 0x1D55C: "\\mathbb{k}", + 0x1D55D: "\\mathbb{l}", + 0x1D55E: "\\mathbb{m}", + 0x1D55F: "\\mathbb{n}", + 0x1D560: "\\mathbb{o}", + 0x1D561: "\\mathbb{p}", + 0x1D562: "\\mathbb{q}", + 0x1D563: "\\mathbb{r}", + 0x1D564: "\\mathbb{s}", + 0x1D565: "\\mathbb{t}", + 0x1D566: "\\mathbb{u}", + 0x1D567: "\\mathbb{v}", + 0x1D568: "\\mathbb{w}", + 0x1D569: "\\mathbb{x}", + 0x1D56A: "\\mathbb{y}", + 0x1D56B: "\\mathbb{z}", + 0x1D56C: "\\mathbfrak{A}", + 0x1D56D: "\\mathbfrak{B}", + 0x1D56E: "\\mathbfrak{C}", + 0x1D56F: "\\mathbfrak{D}", + 0x1D570: "\\mathbfrak{E}", + 0x1D571: "\\mathbfrak{F}", + 0x1D572: "\\mathbfrak{G}", + 0x1D573: "\\mathbfrak{H}", + 0x1D574: "\\mathbfrak{I}", + 0x1D575: "\\mathbfrak{J}", + 0x1D576: "\\mathbfrak{K}", + 0x1D577: "\\mathbfrak{L}", + 0x1D578: "\\mathbfrak{M}", + 0x1D579: "\\mathbfrak{N}", + 0x1D57A: "\\mathbfrak{O}", + 0x1D57B: "\\mathbfrak{P}", + 0x1D57C: "\\mathbfrak{Q}", + 0x1D57D: "\\mathbfrak{R}", + 0x1D57E: "\\mathbfrak{S}", + 0x1D57F: "\\mathbfrak{T}", + 0x1D580: "\\mathbfrak{U}", + 0x1D581: "\\mathbfrak{V}", + 0x1D582: "\\mathbfrak{W}", + 0x1D583: "\\mathbfrak{X}", + 0x1D584: "\\mathbfrak{Y}", + 0x1D585: "\\mathbfrak{Z}", + 0x1D586: "\\mathbfrak{a}", + 0x1D587: "\\mathbfrak{b}", + 0x1D588: "\\mathbfrak{c}", + 0x1D589: "\\mathbfrak{d}", + 0x1D58A: "\\mathbfrak{e}", + 0x1D58B: "\\mathbfrak{f}", + 0x1D58C: "\\mathbfrak{g}", + 0x1D58D: "\\mathbfrak{h}", + 0x1D58E: "\\mathbfrak{i}", + 0x1D58F: "\\mathbfrak{j}", + 0x1D590: "\\mathbfrak{k}", + 0x1D591: "\\mathbfrak{l}", + 0x1D592: "\\mathbfrak{m}", + 0x1D593: "\\mathbfrak{n}", + 0x1D594: "\\mathbfrak{o}", + 0x1D595: "\\mathbfrak{p}", + 0x1D596: "\\mathbfrak{q}", + 0x1D597: "\\mathbfrak{r}", + 0x1D598: "\\mathbfrak{s}", + 0x1D599: "\\mathbfrak{t}", + 0x1D59A: "\\mathbfrak{u}", + 0x1D59B: "\\mathbfrak{v}", + 0x1D59C: "\\mathbfrak{w}", + 0x1D59D: "\\mathbfrak{x}", + 0x1D59E: "\\mathbfrak{y}", + 0x1D59F: "\\mathbfrak{z}", + 0x1D5A0: "\\mathsf{A}", + 0x1D5A1: "\\mathsf{B}", + 0x1D5A2: "\\mathsf{C}", + 0x1D5A3: "\\mathsf{D}", + 0x1D5A4: "\\mathsf{E}", + 0x1D5A5: "\\mathsf{F}", + 0x1D5A6: "\\mathsf{G}", + 0x1D5A7: "\\mathsf{H}", + 0x1D5A8: "\\mathsf{I}", + 0x1D5A9: "\\mathsf{J}", + 0x1D5AA: "\\mathsf{K}", + 0x1D5AB: "\\mathsf{L}", + 0x1D5AC: "\\mathsf{M}", + 0x1D5AD: "\\mathsf{N}", + 0x1D5AE: "\\mathsf{O}", + 0x1D5AF: "\\mathsf{P}", + 0x1D5B0: "\\mathsf{Q}", + 0x1D5B1: "\\mathsf{R}", + 0x1D5B2: "\\mathsf{S}", + 0x1D5B3: "\\mathsf{T}", + 0x1D5B4: "\\mathsf{U}", + 0x1D5B5: "\\mathsf{V}", + 0x1D5B6: "\\mathsf{W}", + 0x1D5B7: "\\mathsf{X}", + 0x1D5B8: "\\mathsf{Y}", + 0x1D5B9: "\\mathsf{Z}", + 0x1D5BA: "\\mathsf{a}", + 0x1D5BB: "\\mathsf{b}", + 0x1D5BC: "\\mathsf{c}", + 0x1D5BD: "\\mathsf{d}", + 0x1D5BE: "\\mathsf{e}", + 0x1D5BF: "\\mathsf{f}", + 0x1D5C0: "\\mathsf{g}", + 0x1D5C1: "\\mathsf{h}", + 0x1D5C2: "\\mathsf{i}", + 0x1D5C3: "\\mathsf{j}", + 0x1D5C4: "\\mathsf{k}", + 0x1D5C5: "\\mathsf{l}", + 0x1D5C6: "\\mathsf{m}", + 0x1D5C7: "\\mathsf{n}", + 0x1D5C8: "\\mathsf{o}", + 0x1D5C9: "\\mathsf{p}", + 0x1D5CA: "\\mathsf{q}", + 0x1D5CB: "\\mathsf{r}", + 0x1D5CC: "\\mathsf{s}", + 0x1D5CD: "\\mathsf{t}", + 0x1D5CE: "\\mathsf{u}", + 0x1D5CF: "\\mathsf{v}", + 0x1D5D0: "\\mathsf{w}", + 0x1D5D1: "\\mathsf{x}", + 0x1D5D2: "\\mathsf{y}", + 0x1D5D3: "\\mathsf{z}", + 0x1D5D4: "\\mathsfbf{A}", + 0x1D5D5: "\\mathsfbf{B}", + 0x1D5D6: "\\mathsfbf{C}", + 0x1D5D7: "\\mathsfbf{D}", + 0x1D5D8: "\\mathsfbf{E}", + 0x1D5D9: "\\mathsfbf{F}", + 0x1D5DA: "\\mathsfbf{G}", + 0x1D5DB: "\\mathsfbf{H}", + 0x1D5DC: "\\mathsfbf{I}", + 0x1D5DD: "\\mathsfbf{J}", + 0x1D5DE: "\\mathsfbf{K}", + 0x1D5DF: "\\mathsfbf{L}", + 0x1D5E0: "\\mathsfbf{M}", + 0x1D5E1: "\\mathsfbf{N}", + 0x1D5E2: "\\mathsfbf{O}", + 0x1D5E3: "\\mathsfbf{P}", + 0x1D5E4: "\\mathsfbf{Q}", + 0x1D5E5: "\\mathsfbf{R}", + 0x1D5E6: "\\mathsfbf{S}", + 0x1D5E7: "\\mathsfbf{T}", + 0x1D5E8: "\\mathsfbf{U}", + 0x1D5E9: "\\mathsfbf{V}", + 0x1D5EA: "\\mathsfbf{W}", + 0x1D5EB: "\\mathsfbf{X}", + 0x1D5EC: "\\mathsfbf{Y}", + 0x1D5ED: "\\mathsfbf{Z}", + 0x1D5EE: "\\mathsfbf{a}", + 0x1D5EF: "\\mathsfbf{b}", + 0x1D5F0: "\\mathsfbf{c}", + 0x1D5F1: "\\mathsfbf{d}", + 0x1D5F2: "\\mathsfbf{e}", + 0x1D5F3: "\\mathsfbf{f}", + 0x1D5F4: "\\mathsfbf{g}", + 0x1D5F5: "\\mathsfbf{h}", + 0x1D5F6: "\\mathsfbf{i}", + 0x1D5F7: "\\mathsfbf{j}", + 0x1D5F8: "\\mathsfbf{k}", + 0x1D5F9: "\\mathsfbf{l}", + 0x1D5FA: "\\mathsfbf{m}", + 0x1D5FB: "\\mathsfbf{n}", + 0x1D5FC: "\\mathsfbf{o}", + 0x1D5FD: "\\mathsfbf{p}", + 0x1D5FE: "\\mathsfbf{q}", + 0x1D5FF: "\\mathsfbf{r}", + 0x1D600: "\\mathsfbf{s}", + 0x1D601: "\\mathsfbf{t}", + 0x1D602: "\\mathsfbf{u}", + 0x1D603: "\\mathsfbf{v}", + 0x1D604: "\\mathsfbf{w}", + 0x1D605: "\\mathsfbf{x}", + 0x1D606: "\\mathsfbf{y}", + 0x1D607: "\\mathsfbf{z}", + 0x1D608: "\\mathsfsl{A}", + 0x1D609: "\\mathsfsl{B}", + 0x1D60A: "\\mathsfsl{C}", + 0x1D60B: "\\mathsfsl{D}", + 0x1D60C: "\\mathsfsl{E}", + 0x1D60D: "\\mathsfsl{F}", + 0x1D60E: "\\mathsfsl{G}", + 0x1D60F: "\\mathsfsl{H}", + 0x1D610: "\\mathsfsl{I}", + 0x1D611: "\\mathsfsl{J}", + 0x1D612: "\\mathsfsl{K}", + 0x1D613: "\\mathsfsl{L}", + 0x1D614: "\\mathsfsl{M}", + 0x1D615: "\\mathsfsl{N}", + 0x1D616: "\\mathsfsl{O}", + 0x1D617: "\\mathsfsl{P}", + 0x1D618: "\\mathsfsl{Q}", + 0x1D619: "\\mathsfsl{R}", + 0x1D61A: "\\mathsfsl{S}", + 0x1D61B: "\\mathsfsl{T}", + 0x1D61C: "\\mathsfsl{U}", + 0x1D61D: "\\mathsfsl{V}", + 0x1D61E: "\\mathsfsl{W}", + 0x1D61F: "\\mathsfsl{X}", + 0x1D620: "\\mathsfsl{Y}", + 0x1D621: "\\mathsfsl{Z}", + 0x1D622: "\\mathsfsl{a}", + 0x1D623: "\\mathsfsl{b}", + 0x1D624: "\\mathsfsl{c}", + 0x1D625: "\\mathsfsl{d}", + 0x1D626: "\\mathsfsl{e}", + 0x1D627: "\\mathsfsl{f}", + 0x1D628: "\\mathsfsl{g}", + 0x1D629: "\\mathsfsl{h}", + 0x1D62A: "\\mathsfsl{i}", + 0x1D62B: "\\mathsfsl{j}", + 0x1D62C: "\\mathsfsl{k}", + 0x1D62D: "\\mathsfsl{l}", + 0x1D62E: "\\mathsfsl{m}", + 0x1D62F: "\\mathsfsl{n}", + 0x1D630: "\\mathsfsl{o}", + 0x1D631: "\\mathsfsl{p}", + 0x1D632: "\\mathsfsl{q}", + 0x1D633: "\\mathsfsl{r}", + 0x1D634: "\\mathsfsl{s}", + 0x1D635: "\\mathsfsl{t}", + 0x1D636: "\\mathsfsl{u}", + 0x1D637: "\\mathsfsl{v}", + 0x1D638: "\\mathsfsl{w}", + 0x1D639: "\\mathsfsl{x}", + 0x1D63A: "\\mathsfsl{y}", + 0x1D63B: "\\mathsfsl{z}", + 0x1D63C: "\\mathsfbfsl{A}", + 0x1D63D: "\\mathsfbfsl{B}", + 0x1D63E: "\\mathsfbfsl{C}", + 0x1D63F: "\\mathsfbfsl{D}", + 0x1D640: "\\mathsfbfsl{E}", + 0x1D641: "\\mathsfbfsl{F}", + 0x1D642: "\\mathsfbfsl{G}", + 0x1D643: "\\mathsfbfsl{H}", + 0x1D644: "\\mathsfbfsl{I}", + 0x1D645: "\\mathsfbfsl{J}", + 0x1D646: "\\mathsfbfsl{K}", + 0x1D647: "\\mathsfbfsl{L}", + 0x1D648: "\\mathsfbfsl{M}", + 0x1D649: "\\mathsfbfsl{N}", + 0x1D64A: "\\mathsfbfsl{O}", + 0x1D64B: "\\mathsfbfsl{P}", + 0x1D64C: "\\mathsfbfsl{Q}", + 0x1D64D: "\\mathsfbfsl{R}", + 0x1D64E: "\\mathsfbfsl{S}", + 0x1D64F: "\\mathsfbfsl{T}", + 0x1D650: "\\mathsfbfsl{U}", + 0x1D651: "\\mathsfbfsl{V}", + 0x1D652: "\\mathsfbfsl{W}", + 0x1D653: "\\mathsfbfsl{X}", + 0x1D654: "\\mathsfbfsl{Y}", + 0x1D655: "\\mathsfbfsl{Z}", + 0x1D656: "\\mathsfbfsl{a}", + 0x1D657: "\\mathsfbfsl{b}", + 0x1D658: "\\mathsfbfsl{c}", + 0x1D659: "\\mathsfbfsl{d}", + 0x1D65A: "\\mathsfbfsl{e}", + 0x1D65B: "\\mathsfbfsl{f}", + 0x1D65C: "\\mathsfbfsl{g}", + 0x1D65D: "\\mathsfbfsl{h}", + 0x1D65E: "\\mathsfbfsl{i}", + 0x1D65F: "\\mathsfbfsl{j}", + 0x1D660: "\\mathsfbfsl{k}", + 0x1D661: "\\mathsfbfsl{l}", + 0x1D662: "\\mathsfbfsl{m}", + 0x1D663: "\\mathsfbfsl{n}", + 0x1D664: "\\mathsfbfsl{o}", + 0x1D665: "\\mathsfbfsl{p}", + 0x1D666: "\\mathsfbfsl{q}", + 0x1D667: "\\mathsfbfsl{r}", + 0x1D668: "\\mathsfbfsl{s}", + 0x1D669: "\\mathsfbfsl{t}", + 0x1D66A: "\\mathsfbfsl{u}", + 0x1D66B: "\\mathsfbfsl{v}", + 0x1D66C: "\\mathsfbfsl{w}", + 0x1D66D: "\\mathsfbfsl{x}", + 0x1D66E: "\\mathsfbfsl{y}", + 0x1D66F: "\\mathsfbfsl{z}", + 0x1D670: "\\mathtt{A}", + 0x1D671: "\\mathtt{B}", + 0x1D672: "\\mathtt{C}", + 0x1D673: "\\mathtt{D}", + 0x1D674: "\\mathtt{E}", + 0x1D675: "\\mathtt{F}", + 0x1D676: "\\mathtt{G}", + 0x1D677: "\\mathtt{H}", + 0x1D678: "\\mathtt{I}", + 0x1D679: "\\mathtt{J}", + 0x1D67A: "\\mathtt{K}", + 0x1D67B: "\\mathtt{L}", + 0x1D67C: "\\mathtt{M}", + 0x1D67D: "\\mathtt{N}", + 0x1D67E: "\\mathtt{O}", + 0x1D67F: "\\mathtt{P}", + 0x1D680: "\\mathtt{Q}", + 0x1D681: "\\mathtt{R}", + 0x1D682: "\\mathtt{S}", + 0x1D683: "\\mathtt{T}", + 0x1D684: "\\mathtt{U}", + 0x1D685: "\\mathtt{V}", + 0x1D686: "\\mathtt{W}", + 0x1D687: "\\mathtt{X}", + 0x1D688: "\\mathtt{Y}", + 0x1D689: "\\mathtt{Z}", + 0x1D68A: "\\mathtt{a}", + 0x1D68B: "\\mathtt{b}", + 0x1D68C: "\\mathtt{c}", + 0x1D68D: "\\mathtt{d}", + 0x1D68E: "\\mathtt{e}", + 0x1D68F: "\\mathtt{f}", + 0x1D690: "\\mathtt{g}", + 0x1D691: "\\mathtt{h}", + 0x1D692: "\\mathtt{i}", + 0x1D693: "\\mathtt{j}", + 0x1D694: "\\mathtt{k}", + 0x1D695: "\\mathtt{l}", + 0x1D696: "\\mathtt{m}", + 0x1D697: "\\mathtt{n}", + 0x1D698: "\\mathtt{o}", + 0x1D699: "\\mathtt{p}", + 0x1D69A: "\\mathtt{q}", + 0x1D69B: "\\mathtt{r}", + 0x1D69C: "\\mathtt{s}", + 0x1D69D: "\\mathtt{t}", + 0x1D69E: "\\mathtt{u}", + 0x1D69F: "\\mathtt{v}", + 0x1D6A0: "\\mathtt{w}", + 0x1D6A1: "\\mathtt{x}", + 0x1D6A2: "\\mathtt{y}", + 0x1D6A3: "\\mathtt{z}", + 0x1D6A8: "\\mathbf{\\Alpha}", + 0x1D6A9: "\\mathbf{\\Beta}", + 0x1D6AA: "\\mathbf{\\Gamma}", + 0x1D6AB: "\\mathbf{\\Delta}", + 0x1D6AC: "\\mathbf{\\Epsilon}", + 0x1D6AD: "\\mathbf{\\Zeta}", + 0x1D6AE: "\\mathbf{\\Eta}", + 0x1D6AF: "\\mathbf{\\Theta}", + 0x1D6B0: "\\mathbf{\\Iota}", + 0x1D6B1: "\\mathbf{\\Kappa}", + 0x1D6B2: "\\mathbf{\\Lambda}", + 0x1D6B3: "\\mathbf{M}", + 0x1D6B4: "N", + 0x1D6B5: "\\mathbf{\\Xi}", + 0x1D6B6: "O", + 0x1D6B7: "\\mathbf{\\Pi}", + 0x1D6B8: "\\mathbf{\\Rho}", + 0x1D6B9: "\\mathbf{\\vartheta}", + 0x1D6BA: "\\mathbf{\\Sigma}", + 0x1D6BB: "\\mathbf{\\Tau}", + 0x1D6BC: "\\mathbf{\\Upsilon}", + 0x1D6BD: "\\mathbf{\\Phi}", + 0x1D6BE: "\\mathbf{\\Chi}", + 0x1D6BF: "\\mathbf{\\Psi}", + 0x1D6C0: "\\mathbf{\\Omega}", + 0x1D6C1: "\\mathbf{\\nabla}", + 0x1D6C2: "\\mathbf{\\alpha}", + 0x1D6C3: "\\mathbf{\\beta}", + 0x1D6C4: "\\mathbf{\\gamma}", + 0x1D6C5: "\\mathbf{\\delta}", + 0x1D6C6: "\\mathbf{\\epsilon}", + 0x1D6C7: "\\mathbf{\\zeta}", + 0x1D6C8: "\\mathbf{\\eta}", + 0x1D6C9: "\\mathbf{\\theta}", + 0x1D6CA: "\\mathbf{\\iota}", + 0x1D6CB: "\\mathbf{\\kappa}", + 0x1D6CC: "\\mathbf{\\lambda}", + 0x1D6CD: "\\mathbf{\\mu}", + 0x1D6CE: "\\mathbf{\\nu}", + 0x1D6CF: "\\mathbf{\\xi}", + 0x1D6D0: "\\mathbf{o}", + 0x1D6D1: "\\mathbf{\\pi}", + 0x1D6D2: "\\mathbf{\\rho}", + 0x1D6D3: "\\mathbf{\\varsigma}", + 0x1D6D4: "\\mathbf{\\sigma}", + 0x1D6D5: "\\mathbf{\\tau}", + 0x1D6D6: "\\mathbf{\\upsilon}", + 0x1D6D7: "\\mathbf{\\phi}", + 0x1D6D8: "\\mathbf{\\chi}", + 0x1D6D9: "\\mathbf{\\psi}", + 0x1D6DA: "\\mathbf{\\omega}", + 0x1D6DB: "\\partial", + 0x1D6DC: "\\mathbf{\\varepsilon}", + 0x1D6DD: "\\mathbf{\\vartheta}", + 0x1D6DE: "\\mathbf{\\varkappa}", + 0x1D6DF: "\\mathbf{\\phi}", + 0x1D6E0: "\\mathbf{\\varrho}", + 0x1D6E1: "\\mathbf{\\varpi}", + 0x1D6E2: "\\mathmit{\\Alpha}", + 0x1D6E3: "\\mathmit{\\Beta}", + 0x1D6E4: "\\mathmit{\\Gamma}", + 0x1D6E5: "\\mathmit{\\Delta}", + 0x1D6E6: "\\mathmit{\\Epsilon}", + 0x1D6E7: "\\mathmit{\\Zeta}", + 0x1D6E8: "\\mathmit{\\Eta}", + 0x1D6E9: "\\mathmit{\\Theta}", + 0x1D6EA: "\\mathmit{\\Iota}", + 0x1D6EB: "\\mathmit{\\Kappa}", + 0x1D6EC: "\\mathmit{\\Lambda}", + 0x1D6ED: "\\mathmit{M}", + 0x1D6EE: "N", + 0x1D6EF: "\\mathmit{\\Xi}", + 0x1D6F0: "O", + 0x1D6F1: "\\mathmit{\\Pi}", + 0x1D6F2: "\\mathmit{\\Rho}", + 0x1D6F3: "\\mathmit{\\vartheta}", + 0x1D6F4: "\\mathmit{\\Sigma}", + 0x1D6F5: "\\mathmit{\\Tau}", + 0x1D6F6: "\\mathmit{\\Upsilon}", + 0x1D6F7: "\\mathmit{\\Phi}", + 0x1D6F8: "\\mathmit{\\Chi}", + 0x1D6F9: "\\mathmit{\\Psi}", + 0x1D6FA: "\\mathmit{\\Omega}", + 0x1D6FB: "\\mathmit{\\nabla}", + 0x1D6FC: "\\mathmit{\\alpha}", + 0x1D6FD: "\\mathmit{\\beta}", + 0x1D6FE: "\\mathmit{\\gamma}", + 0x1D6FF: "\\mathmit{\\delta}", + 0x1D700: "\\mathmit{\\epsilon}", + 0x1D701: "\\mathmit{\\zeta}", + 0x1D702: "\\mathmit{\\eta}", + 0x1D703: "\\mathmit{\\theta}", + 0x1D704: "\\mathmit{\\iota}", + 0x1D705: "\\mathmit{\\kappa}", + 0x1D706: "\\mathmit{\\lambda}", + 0x1D707: "\\mathmit{\\mu}", + 0x1D708: "\\mathmit{\\nu}", + 0x1D709: "\\mathmit{\\xi}", + 0x1D70A: "\\mathmit{o}", + 0x1D70B: "\\mathmit{\\pi}", + 0x1D70C: "\\mathmit{\\rho}", + 0x1D70D: "\\mathmit{\\varsigma}", + 0x1D70E: "\\mathmit{\\sigma}", + 0x1D70F: "\\mathmit{\\tau}", + 0x1D710: "\\mathmit{\\upsilon}", + 0x1D711: "\\mathmit{\\phi}", + 0x1D712: "\\mathmit{\\chi}", + 0x1D713: "\\mathmit{\\psi}", + 0x1D714: "\\mathmit{\\omega}", + 0x1D715: "\\partial", + 0x1D716: "\\in", + 0x1D717: "\\mathmit{\\vartheta}", + 0x1D718: "\\mathmit{\\varkappa}", + 0x1D719: "\\mathmit{\\phi}", + 0x1D71A: "\\mathmit{\\varrho}", + 0x1D71B: "\\mathmit{\\varpi}", + 0x1D71C: "\\mathbit{\\Alpha}", + 0x1D71D: "\\mathbit{\\Beta}", + 0x1D71E: "\\mathbit{\\Gamma}", + 0x1D71F: "\\mathbit{\\Delta}", + 0x1D720: "\\mathbit{\\Epsilon}", + 0x1D721: "\\mathbit{\\Zeta}", + 0x1D722: "\\mathbit{\\Eta}", + 0x1D723: "\\mathbit{\\Theta}", + 0x1D724: "\\mathbit{\\Iota}", + 0x1D725: "\\mathbit{\\Kappa}", + 0x1D726: "\\mathbit{\\Lambda}", + 0x1D727: "\\mathbit{M}", + 0x1D728: "\\mathbit{N}", + 0x1D729: "\\mathbit{\\Xi}", + 0x1D72A: "O", + 0x1D72B: "\\mathbit{\\Pi}", + 0x1D72C: "\\mathbit{\\Rho}", + 0x1D72D: "\\mathbit{O}", + 0x1D72E: "\\mathbit{\\Sigma}", + 0x1D72F: "\\mathbit{\\Tau}", + 0x1D730: "\\mathbit{\\Upsilon}", + 0x1D731: "\\mathbit{\\Phi}", + 0x1D732: "\\mathbit{\\Chi}", + 0x1D733: "\\mathbit{\\Psi}", + 0x1D734: "\\mathbit{\\Omega}", + 0x1D735: "\\mathbit{\\nabla}", + 0x1D736: "\\mathbit{\\alpha}", + 0x1D737: "\\mathbit{\\beta}", + 0x1D738: "\\mathbit{\\gamma}", + 0x1D739: "\\mathbit{\\delta}", + 0x1D73A: "\\mathbit{\\epsilon}", + 0x1D73B: "\\mathbit{\\zeta}", + 0x1D73C: "\\mathbit{\\eta}", + 0x1D73D: "\\mathbit{\\theta}", + 0x1D73E: "\\mathbit{\\iota}", + 0x1D73F: "\\mathbit{\\kappa}", + 0x1D740: "\\mathbit{\\lambda}", + 0x1D741: "\\mathbit{\\mu}", + 0x1D742: "\\mathbit{\\nu}", + 0x1D743: "\\mathbit{\\xi}", + 0x1D744: "\\mathbit{o}", + 0x1D745: "\\mathbit{\\pi}", + 0x1D746: "\\mathbit{\\rho}", + 0x1D747: "\\mathbit{\\varsigma}", + 0x1D748: "\\mathbit{\\sigma}", + 0x1D749: "\\mathbit{\\tau}", + 0x1D74A: "\\mathbit{\\upsilon}", + 0x1D74B: "\\mathbit{\\phi}", + 0x1D74C: "\\mathbit{\\chi}", + 0x1D74D: "\\mathbit{\\psi}", + 0x1D74E: "\\mathbit{\\omega}", + 0x1D74F: "\\partial", + 0x1D750: "\\in", + 0x1D751: "\\mathbit{\\vartheta}", + 0x1D752: "\\mathbit{\\varkappa}", + 0x1D753: "\\mathbit{\\phi}", + 0x1D754: "\\mathbit{\\varrho}", + 0x1D755: "\\mathbit{\\varpi}", + 0x1D756: "\\mathsfbf{\\Alpha}", + 0x1D757: "\\mathsfbf{\\Beta}", + 0x1D758: "\\mathsfbf{\\Gamma}", + 0x1D759: "\\mathsfbf{\\Delta}", + 0x1D75A: "\\mathsfbf{\\Epsilon}", + 0x1D75B: "\\mathsfbf{\\Zeta}", + 0x1D75C: "\\mathsfbf{\\Eta}", + 0x1D75D: "\\mathsfbf{\\Theta}", + 0x1D75E: "\\mathsfbf{\\Iota}", + 0x1D75F: "\\mathsfbf{\\Kappa}", + 0x1D760: "\\mathsfbf{\\Lambda}", + 0x1D761: "\\mathsfbf{M}", + 0x1D762: "\\mathsfbf{N}", + 0x1D763: "\\mathsfbf{\\Xi}", + 0x1D764: "O", + 0x1D765: "\\mathsfbf{\\Pi}", + 0x1D766: "\\mathsfbf{\\Rho}", + 0x1D767: "\\mathsfbf{\\vartheta}", + 0x1D768: "\\mathsfbf{\\Sigma}", + 0x1D769: "\\mathsfbf{\\Tau}", + 0x1D76A: "\\mathsfbf{\\Upsilon}", + 0x1D76B: "\\mathsfbf{\\Phi}", + 0x1D76C: "\\mathsfbf{\\Chi}", + 0x1D76D: "\\mathsfbf{\\Psi}", + 0x1D76E: "\\mathsfbf{\\Omega}", + 0x1D76F: "\\mathsfbf{\\nabla}", + 0x1D770: "\\mathsfbf{\\alpha}", + 0x1D771: "\\mathsfbf{\\beta}", + 0x1D772: "\\mathsfbf{\\gamma}", + 0x1D773: "\\mathsfbf{\\delta}", + 0x1D774: "\\mathsfbf{\\epsilon}", + 0x1D775: "\\mathsfbf{\\zeta}", + 0x1D776: "\\mathsfbf{\\eta}", + 0x1D777: "\\mathsfbf{\\theta}", + 0x1D778: "\\mathsfbf{\\iota}", + 0x1D779: "\\mathsfbf{\\kappa}", + 0x1D77A: "\\mathsfbf{\\lambda}", + 0x1D77B: "\\mathsfbf{\\mu}", + 0x1D77C: "\\mathsfbf{\\nu}", + 0x1D77D: "\\mathsfbf{\\xi}", + 0x1D77E: "\\mathsfbf{o}", + 0x1D77F: "\\mathsfbf{\\pi}", + 0x1D780: "\\mathsfbf{\\rho}", + 0x1D781: "\\mathsfbf{\\varsigma}", + 0x1D782: "\\mathsfbf{\\sigma}", + 0x1D783: "\\mathsfbf{\\tau}", + 0x1D784: "\\mathsfbf{\\upsilon}", + 0x1D785: "\\mathsfbf{\\phi}", + 0x1D786: "\\mathsfbf{\\chi}", + 0x1D787: "\\mathsfbf{\\psi}", + 0x1D788: "\\mathsfbf{\\omega}", + 0x1D789: "\\partial", + 0x1D78A: "\\mathsfbf{\\varepsilon}", + 0x1D78B: "\\mathsfbf{\\vartheta}", + 0x1D78C: "\\mathsfbf{\\varkappa}", + 0x1D78D: "\\mathsfbf{\\phi}", + 0x1D78E: "\\mathsfbf{\\varrho}", + 0x1D78F: "\\mathsfbf{\\varpi}", + 0x1D790: "\\mathsfbfsl{\\Alpha}", + 0x1D791: "\\mathsfbfsl{\\Beta}", + 0x1D792: "\\mathsfbfsl{\\Gamma}", + 0x1D793: "\\mathsfbfsl{\\Delta}", + 0x1D794: "\\mathsfbfsl{\\Epsilon}", + 0x1D795: "\\mathsfbfsl{\\Zeta}", + 0x1D796: "\\mathsfbfsl{\\Eta}", + 0x1D797: "\\mathsfbfsl{\\vartheta}", + 0x1D798: "\\mathsfbfsl{\\Iota}", + 0x1D799: "\\mathsfbfsl{\\Kappa}", + 0x1D79A: "\\mathsfbfsl{\\Lambda}", + 0x1D79B: "\\mathsfbfsl{M}", + 0x1D79C: "\\mathsfbfsl{N}", + 0x1D79D: "\\mathsfbfsl{\\Xi}", + 0x1D79E: "O", + 0x1D79F: "\\mathsfbfsl{\\Pi}", + 0x1D7A0: "\\mathsfbfsl{\\Rho}", + 0x1D7A1: "\\mathsfbfsl{\\vartheta}", + 0x1D7A2: "\\mathsfbfsl{\\Sigma}", + 0x1D7A3: "\\mathsfbfsl{\\Tau}", + 0x1D7A4: "\\mathsfbfsl{\\Upsilon}", + 0x1D7A5: "\\mathsfbfsl{\\Phi}", + 0x1D7A6: "\\mathsfbfsl{\\Chi}", + 0x1D7A7: "\\mathsfbfsl{\\Psi}", + 0x1D7A8: "\\mathsfbfsl{\\Omega}", + 0x1D7A9: "\\mathsfbfsl{\\nabla}", + 0x1D7AA: "\\mathsfbfsl{\\alpha}", + 0x1D7AB: "\\mathsfbfsl{\\beta}", + 0x1D7AC: "\\mathsfbfsl{\\gamma}", + 0x1D7AD: "\\mathsfbfsl{\\delta}", + 0x1D7AE: "\\mathsfbfsl{\\epsilon}", + 0x1D7AF: "\\mathsfbfsl{\\zeta}", + 0x1D7B0: "\\mathsfbfsl{\\eta}", + 0x1D7B1: "\\mathsfbfsl{\\vartheta}", + 0x1D7B2: "\\mathsfbfsl{\\iota}", + 0x1D7B3: "\\mathsfbfsl{\\kappa}", + 0x1D7B4: "\\mathsfbfsl{\\lambda}", + 0x1D7B5: "\\mathsfbfsl{\\mu}", + 0x1D7B6: "\\mathsfbfsl{\\nu}", + 0x1D7B7: "\\mathsfbfsl{\\xi}", + 0x1D7B8: "\\mathsfbfsl{o}", + 0x1D7B9: "\\mathsfbfsl{\\pi}", + 0x1D7BA: "\\mathsfbfsl{\\rho}", + 0x1D7BB: "\\mathsfbfsl{\\varsigma}", + 0x1D7BC: "\\mathsfbfsl{\\sigma}", + 0x1D7BD: "\\mathsfbfsl{\\tau}", + 0x1D7BE: "\\mathsfbfsl{\\upsilon}", + 0x1D7BF: "\\mathsfbfsl{\\phi}", + 0x1D7C0: "\\mathsfbfsl{\\chi}", + 0x1D7C1: "\\mathsfbfsl{\\psi}", + 0x1D7C2: "\\mathsfbfsl{\\omega}", + 0x1D7C3: "\\partial", + 0x1D7C4: "\\in", + 0x1D7C5: "\\mathsfbfsl{\\vartheta}", + 0x1D7C6: "\\mathsfbfsl{\\varkappa}", + 0x1D7C7: "\\mathsfbfsl{\\phi}", + 0x1D7C8: "\\mathsfbfsl{\\varrho}", + 0x1D7C9: "\\mathsfbfsl{\\varpi}", + 0x1D7CE: "\\mathbf{0}", + 0x1D7CF: "\\mathbf{1}", + 0x1D7D0: "\\mathbf{2}", + 0x1D7D1: "\\mathbf{3}", + 0x1D7D2: "\\mathbf{4}", + 0x1D7D3: "\\mathbf{5}", + 0x1D7D4: "\\mathbf{6}", + 0x1D7D5: "\\mathbf{7}", + 0x1D7D6: "\\mathbf{8}", + 0x1D7D7: "\\mathbf{9}", + 0x1D7D8: "\\mathbb{0}", + 0x1D7D9: "\\mathbb{1}", + 0x1D7DA: "\\mathbb{2}", + 0x1D7DB: "\\mathbb{3}", + 0x1D7DC: "\\mathbb{4}", + 0x1D7DD: "\\mathbb{5}", + 0x1D7DE: "\\mathbb{6}", + 0x1D7DF: "\\mathbb{7}", + 0x1D7E0: "\\mathbb{8}", + 0x1D7E1: "\\mathbb{9}", + 0x1D7E2: "\\mathsf{0}", + 0x1D7E3: "\\mathsf{1}", + 0x1D7E4: "\\mathsf{2}", + 0x1D7E5: "\\mathsf{3}", + 0x1D7E6: "\\mathsf{4}", + 0x1D7E7: "\\mathsf{5}", + 0x1D7E8: "\\mathsf{6}", + 0x1D7E9: "\\mathsf{7}", + 0x1D7EA: "\\mathsf{8}", + 0x1D7EB: "\\mathsf{9}", + 0x1D7EC: "\\mathsfbf{0}", + 0x1D7ED: "\\mathsfbf{1}", + 0x1D7EE: "\\mathsfbf{2}", + 0x1D7EF: "\\mathsfbf{3}", + 0x1D7F0: "\\mathsfbf{4}", + 0x1D7F1: "\\mathsfbf{5}", + 0x1D7F2: "\\mathsfbf{6}", + 0x1D7F3: "\\mathsfbf{7}", + 0x1D7F4: "\\mathsfbf{8}", + 0x1D7F5: "\\mathsfbf{9}", + 0x1D7F6: "\\mathtt{0}", + 0x1D7F7: "\\mathtt{1}", + 0x1D7F8: "\\mathtt{2}", + 0x1D7F9: "\\mathtt{3}", + 0x1D7FA: "\\mathtt{4}", + 0x1D7FB: "\\mathtt{5}", + 0x1D7FC: "\\mathtt{6}", + 0x1D7FD: "\\mathtt{7}", + 0x1D7FE: "\\mathtt{8}", + 0x1D7FF: "\\mathtt{9}", +} diff --git a/good_ai/src/sus/external/pylatexenc/latexencode/_unicode_to_latex_encoder.py b/good_ai/src/sus/external/pylatexenc/latexencode/_unicode_to_latex_encoder.py new file mode 100644 index 0000000..fbc5a47 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexencode/_unicode_to_latex_encoder.py @@ -0,0 +1,686 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2021 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from __future__ import absolute_import, print_function, unicode_literals + +import functools +import itertools +import logging +import sys +import unicodedata + +if sys.version_info.major > 2: + unicode = str # need to support unicode() w/ no arguments + basestring = str + # use MappingProxyType for keeping + # inspect function argument names + from inspect import getfullargspec + from types import MappingProxyType as _MappingProxyType +else: + _MappingProxyType = dict + # inspect function argument names -- simulate getfullargspec with getargspec (argh...) + from inspect import getargspec as getfullargspec + +logger = logging.getLogger(__name__) + + +def get_builtin_uni2latex_dict(): + r""" + Return a dictionary that contains the default collection of known LaTeX + escape sequences for unicode characters. + + The keys of the dictionary are integers that correspond to unicode code + points (i.e., `ord(char)`). The values are the corresponding LaTeX + replacement strings. + + The returned dictionary may not be modified. To alter the behavior of + :py:func:`unicode_to_latex()`, you should specify custom rules to a new + instance of :py:class:`UnicodeToLatexEncoder`. + + .. versionadded:: 2.0 + + This function was introduced in `pylatexenc 2.0`. + """ + from ._uni2latexmap import uni2latex as _uni2latex + + return _MappingProxyType(_uni2latex) + + +RULE_DICT = 0 +r""" +Indicates a rule type that is a dictionary of unicode point values to +replacement strings. See :py:class:`UnicodeToLatexConversionRule`. + +.. versionadded:: 2.0 + + This member was introduced in pylatexenc version 2.0. +""" + +RULE_REGEX = 1 +r""" +Indicates a rule type that is a list (or iterable) of pairs +`(compiled_regular_expression, replacement_string)`. See +:py:class:`UnicodeToLatexConversionRule`. + +.. versionadded:: 2.0 + + This member was introduced in pylatexenc version 2.0. +""" + +RULE_CALLABLE = 2 +r""" +Indicates a rule type that is a custom callable. See +:py:class:`UnicodeToLatexConversionRule`. + +.. versionadded:: 2.0 + + This member was introduced in pylatexenc version 2.0. +""" + + +class UnicodeToLatexConversionRule: + r""" + Specify a rule how to convert unicode characters into LaTeX escapes. + + .. py:attribute:: rule_type + + One of :py:data:`RULE_DICT`, :py:data:`RULE_REGEX`, or + :py:data:`RULE_CALLABLE`. + + .. py:attribute:: rule + + A specification of the rule itself. The `rule` attribute is an object + that depends on what `rule_type` is set to. See below. + + .. py:attribute:: replacement_latex_protection + + If non-`None`, then the setting here will override any + `replacement_latex_protection` set on + :py:class:`UnicodeToLatexConversionRule` objects. By default the value + is `None`, and you can set a replacement_latex_protection globally for + all rules on the :py:class:`UnicodeToLatexEncoder` object. + + The use of this attribute is mainly in case you have a fancy rule in + which you already guarantee that whatever you output is valid LaTeX even + if concatenated with the remainder of the string; in this case you can + set `replacement_latex_protection='none'` to avoid unnecessary or + unwanted braces around the generated code. + + .. versionadded:: 2.10 + + The `replacement_latex_protection` attribute was introduced in + `pylatexenc 2.10`. + + + Constructor syntax:: + + UnicodeToLatexConversionRule(RULE_XXX, <...>) + UnicodeToLatexConversionRule(rule_type=RULE_XXX, rule=<...>) + + UnicodeToLatexConversionRule(..., replacement_latex_protection='none') + + Note that you can get some built-in rules via the + :py:func:`get_builtin_conversion_rules()` function:: + + conversion_rules = get_builtin_conversion_rules('defaults') # all defaults + + + Rules types: + + - `RULE_DICT`: If `rule_type` is `RULE_DICT`, then `rule` should be a + dictionary whose keys are integers representing unicode code points + (e.g., `0x210F`), and whose values are corresponding replacement strings + (e.g., ``r'\hbar'``). See :py:func:`get_builtin_uni2latex_dict()` for + an example. + + - `RULE_REGEX`: If `rule_type` is `RULE_REGEX`, then `rule` should be an + iterable of tuple pairs `(compiled_regular_expression, + replacement_string)` where `compiled_regular_expression` was obtained + with `re.compile(...)` and `replacement_string` is anything that can be + specified as the second (`repl`) argument of `re.sub(...)`. This can be + a replacement string that includes escapes (like ``\1, \2, \g``) + for captured sub-expressions or a callable that takes a match object as + argument. + + .. note:: + + The replacement string is parsed like the second argument to + `re.sub()` and backslashes have a special meaning because they can + refer to captured sub-expressions. For a literal backslash, use two + backslashes ``\\`` in raw strings, four backslashes in normal + strings. + + Example:: + + regex_conversion_rule = UnicodeToLatexConversionRule( + rule_type=RULE_REGEX, + rule=[ + # protect acronyms of capital letters with braces, + # e.g.: ABC -> {ABC} + (re.compile(r'[A-Z]{2,}'), r'{\1}'), + # Additional rules, e.g., "..." -> "\ldots" + (re.compile(r'...'), r'\\ldots'), # note double \\ + ] + ) + + - `RULE_CALLABLE`: If `rule_type` is `RULE_CALLABLE`, then `rule` should + be a callable that accepts two arguments, the unicode string and the + position in the string (an integer). The callable will be called with + the original unicode string as argument and the position of the + character that needs to be encoded. If this rule can encode the given + character at the given position, it should return a tuple + `(consumed_length, replacement_string)` where `consumed_length` is the + number of characters in the unicode string that `replacement_string` + represents. If the character(s) at the given position can't be encoded + by this rule, the callable should return `None` to indicate that further + rules should be attempted. + + If the callable accepts an additional argument called `u2lobj`, then the + :py:class:`UnicodeToLatexEncoder` instance is provided to that argument. + + For example, the following callable should achieve the same effect as + the previous example with regexes:: + + def convert_stuff(s, pos): + m = re.match(r'[A-Z]{2,}', s, pos) + if m is not None: + return (m.end()-m.start(), '{'+m.group()+'}') + if s.startswith('...', pos): # or s[pos:pos+3] == '...' + return (3, r'\ldots') + return None + + + .. versionadded:: 2.0 + + This class was introduced in `pylatexenc 2.0`. + """ + + def __init__( + self, + rule_type, + rule=None, + # keyword-only, please: + replacement_latex_protection=None, + ): + self.rule_type = rule_type + self.rule = rule + self.replacement_latex_protection = replacement_latex_protection + + def __repr__(self): + return "{}(rule_type={!r}, rule=<{}>, replacement_latex_protection={})".format( + self.__class__.__name__, + self.rule_type, + type(self.rule).__name__, + repr(self.replacement_latex_protection), + ) + + +def get_builtin_conversion_rules(builtin_name): + r""" + Return a built-in set of conversion rules specified by a given name + `builtin_name`. + + There are two builtin conversion rules, with the following names: + + - `'defaults'`: the default conversion rules, a custom-curated list of + unicode chars to LaTeX escapes. + + - `'unicode-xml'`: the conversion rules derived from the `unicode.xml` file + maintained at https://www.w3.org/TR/xml-entity-names/#source by David + Carlisle. + + The return value is a list of :py:class:`UnicodeToLatexConversionRule` + objects that can be either directly specified to the `conversion_rules=` + argument of :py:class:`UnicodeToLatexEncoder`, or included in a larger list + that can be provided to that argument. + + .. versionadded:: 2.0 + + This function was introduced in `pylatexenc 2.0`. + """ + if builtin_name == "defaults": + return [ + UnicodeToLatexConversionRule( + rule_type=RULE_DICT, rule=get_builtin_uni2latex_dict() + ) + ] + if builtin_name == "unicode-xml": + from . import _uni2latexmap_xml + + return [ + UnicodeToLatexConversionRule( + rule_type=RULE_DICT, rule=_uni2latexmap_xml.uni2latex + ) + ] + raise ValueError("Unknown builtin rule set: {}".format(builtin_name)) + + +class UnicodeToLatexEncoder(object): + r""" + Encode a string with unicode characters into a LaTeX snippet. + + The following general attributes can be specified as keyword arguments to + the constructor. Note: These attributes must be specified to the + constructor and may NOT be subsequently modified. This is because in the + constructor we pre-compile some rules and flags to optimize calls to + :py:meth:`unicode_to_text()`. + + .. py:attribute:: non_ascii_only + + Whether we should convert only non-ascii characters into LaTeX sequences, + or also all known ascii characters with special LaTeX meaning such as + '\\\\', '$', '&', etc. + + If `non_ascii_only` is set to `True` (the default is `False`), then + conversion rules are not applied at positions in the string where an + ASCII character is encountered. + + .. py:attribute:: conversion_rules + + The conversion rules, specified as a list of + :py:class:`UnicodeToLatexConversionRule` objects. For each position in + the string, the rules will be applied in the given sequence until a + replacement string is found. + + Instead of a :py:class:`UnicodeToLatexConversionRule` object you may also + specify a string specifying a built-in rule (e.g., 'defaults'), which + will be expanded to the corresponding rules according to + :py:func:`get_builtin_conversion_rules()`. + + If you specify your own list of rules using this argument, you will + probably want to include presumably at the end of your list the element + 'defaults' to include all built-in default conversion rules. To override + built-in rules, simply add your custom rules earlier in the list. + Example:: + + conversion_rules = [ + # our custom rules + UnicodeToLatexConversionRule(RULE_REGEX, [ + # double \\ needed, see UnicodeToLatexConversionRule + ( re.compile(r'...'), r'\\ldots' ), + ( re.compile(r'î'), r'\\^i' ), + ]), + # plus all the default rules + 'defaults' + ] + u = UnicodeToLatexEncoder(conversion_rules=conversion_rules) + + .. py:attribute:: replacement_latex_protection + + How to "protect" LaTeX replacement text that looks like it could be + interpreted differently if concatenated to arbitrary strings before and + after. + + Currently in the default scheme only one situation is recognized: if the + replacement string ends with a latex macro invocation with a non-symbol + macro name, e.g. ``\textemdash`` or ``\^\i``. Indeed, if we naively + replace these texts in an arbitrary string (like ``maître``), we might + get an invalid macro invocation (like ``ma\^\itre`` which causes un known + macro name ``\itre``). + + Possible protection schemes are: + + - 'braces' (the default): Any suspicious replacement text (that + might look fragile) is placed in curly braces ``{...}``. + + - 'braces-all': All replacement latex escapes are surrounded in + protective curly braces ``{...}``, regardless of whether or not they + might be deemed "fragile" or "unsafe". + + - 'braces-almost-all': Almost all replacement latex escapes are + surrounded in protective curly braces ``{...}``. This option + emulates closely the behavior of `brackets=True` of the function + `utf8tolatex()` in `pylatexenc 1.x`, though I'm not sure it is really + useful. [Specifically, all those replacement strings that start with + a backslash are surrounded by curly braces]. + + - 'braces-after-macro': In the situation where the replacement latex + code ends with a string-named macro, then a pair of empty braces is + added at the end of the replacement text to protect the macro. + + - 'none': No protection is applied, even in "unsafe" cases. This is + not recommended, as this will likely result in invalid LaTeX + code. (Note this is the string 'none', not Python's built-in `None`.) + + - any callable object: The callable should take a single argument, the + replacement latex string associated with a piece of the input (maybe + a special character) that has been encoded; it should return the + actual string to append to the output string. + + .. versionadded:: 2.10 + + You can specify a callable object to `replacement_latex_protection` + since `pylatexenc 2.10`. + + .. py:attribute:: unknown_char_policy + + What to do when a non-ascii character is encountered without any known + substitution macro. The attribute `unknown_char_policy` can be set to one of: + + - 'keep': keep the character as is; + + - 'replace': replace the character by a boldface question mark; + + - 'ignore': ignore the character from the input entirely and don't + output anything for it; + + - 'fail': raise a `ValueError` exception; + + - 'unihex': output the unicode hexadecimal code (U+XXXX) of the + character in typewriter font; + + - a Python callable --- will be called with argument the character that + could not be encoded. (If the callable accepts a second argument + called 'u2lobj', then the `UnicodeToLatexEncoder` instance is + provided to that argument.) The return value of the callable is used + as LaTeX replacement code. + + .. py:attribute:: unknown_char_warning + + In addition to the `unknown_char_policy`, this attribute indicates + whether or not (`True` or `False`) one should generate a warning when a + nonascii character without any known latex representation is + encountered. (Default: True) + + .. py:attribute:: latex_string_class + + The return type of :py:meth:`unicode_to_latex()`. Normally this is a + simple unicode string (`str` on `Python 3` or `unicode` on `Python 2`). + + But you can specify your custom string type via the `latex_string_class` + argument. The `latex_string_class` will be invoked with no arguments to + construct an empty object (so `latex_string_class` can be either an + object that can be constructed with no arguments or it can be a function + with no arguments that return a fresh object instance). The object must + support the operation "+=", i.e., you should overload the ``__iadd__()`` + method. + + For instance, you can record the chunks that would have been appended + into a single string as follows:: + + class LatexChunkList: + def __init__(self): + self.chunks = [] + + def __iadd__(self, s): + self.chunks.append(s) + return self + + u = UnicodeToLatexEncoder(latex_string_class=LatexChunkList, + replacement_latex_protection='none') + result = u.unicode_to_latex("é → α") + # result.chunks == [ r"\'e", ' ', r'\textrightarrow', ' ', + # r'\ensuremath{\alpha}' ] + + .. warning:: + + None of the above attributes should be modified after constructing the + object. The values specified to the class constructor are final and + cannot be changed. [Indeed, the class constructor "compiles" these + attribute values into a data structure that makes + :py:meth:`unicode_to_text()` slightly more efficient.] + + .. versionadded:: 2.0 + + This class was introduced in `pylatexenc 2.0`. + """ + + def __init__(self, **kwargs): + self.non_ascii_only = kwargs.pop("non_ascii_only", False) + self.conversion_rules = kwargs.pop("conversion_rules", ["defaults"]) + self.replacement_latex_protection = kwargs.pop( + "replacement_latex_protection", "braces" + ) + self.unknown_char_policy = kwargs.pop("unknown_char_policy", "keep") + self.unknown_char_warning = kwargs.pop("unknown_char_warning", True) + self.latex_string_class = kwargs.pop("latex_string_class", unicode) + + if kwargs: + logger.warning( + "Ignoring unknown keyword arguments: %s", ",".join(kwargs.keys()) + ) + + super(UnicodeToLatexEncoder, self).__init__(**kwargs) + + # build generator that expands built-in conversion rules + expanded_conversion_rules = itertools.chain.from_iterable( + (get_builtin_conversion_rules(r) if isinstance(r, basestring) else [r]) + for r in self.conversion_rules + ) + + # + # now "pre-compile" some stuff so that calls to unicode_to_latex() can + # hopefully execute faster + # + + # "pre-compile" rules and check rule types: + self._compiled_rules = [] + for rule in expanded_conversion_rules: + if rule.rule_type == RULE_DICT: + self._compiled_rules.append( + functools.partial(self._apply_rule_dict, rule.rule, rule) + ) + elif rule.rule_type == RULE_REGEX: + self._compiled_rules.append( + functools.partial(self._apply_rule_regex, rule.rule, rule) + ) + elif rule.rule_type == RULE_CALLABLE: + thecallable = rule.rule + if "u2lobj" in getfullargspec(thecallable)[0]: + thecallable = functools.partial(rule.rule, u2lobj=self) + self._compiled_rules.append( + functools.partial(self._apply_rule_callable, thecallable, rule) + ) + else: + raise TypeError("Invalid rule type: {}".format(rule.rule_type)) + + # bad char policy: + if isinstance(self.unknown_char_policy, basestring): + self._do_unknown_char = self._get_method_fn( + "do_unknown_char", self.unknown_char_policy, what="unknown_char_policy" + ) + elif callable(self.unknown_char_policy): + fn = self.unknown_char_policy + if "u2lobj" in getfullargspec(fn)[0]: + self._do_unknown_char = functools.partial( + self.unknown_char_policy, u2lobj=self + ) + else: + self._do_unknown_char = self.unknown_char_policy + else: + raise TypeError( + "Invalid argument for unknown_char_policy: {!r}".format( + self.unknown_char_policy + ) + ) + + # bad char warning: + if not self.unknown_char_warning: + self._do_warn_unknown_char = lambda ch: None # replace method by no-op + + # set a method that will skip ascii characters if required: + if self.non_ascii_only: + self._maybe_skip_ascii = self._check_do_skip_ascii + else: + self._maybe_skip_ascii = lambda s, p: False + + # set a method to protect replacement latex code, if necessary: + self._apply_protection = self._get_replacement_latex_fn( + self.replacement_latex_protection + ) + + def _get_method_fn(self, base, name, what): + selfmethname = "_" + base + "_" + name.replace("-", "_") + if not hasattr(self, selfmethname): + raise ValueError("Invalid {}: {}".format(what, name)) + return getattr(self, selfmethname) + + def _get_replacement_latex_fn(self, replacement_latex_protection): + if callable(replacement_latex_protection): + return replacement_latex_protection + return self._get_method_fn( + "apply_protection", + replacement_latex_protection, + what="replacement_latex_protection", + ) + + def unicode_to_latex(self, s): + """ + Convert unicode characters in the string `s` into latex escape sequences, + according to the rules and options given to the constructor. + """ + + s = unicode(s) # make sure s is unicode + s = unicodedata.normalize("NFC", s) + + class _NS: + pass + + p = _NS() + p.latex = self.latex_string_class() + p.pos = 0 + + while p.pos < len(s): + + if self._maybe_skip_ascii(s, p): + continue + + for compiledrule in self._compiled_rules: + if compiledrule(s, p): + break + else: + # for-else, see + # https://docs.python.org/2/tutorial/controlflow.html\ + # #break-and-continue-statements-and-else-clauses-on-loops + ch = s[p.pos] + o = ord(ch) + if (o >= 32 and o <= 127) or (ch in "\n\r\t"): + p.latex += ch + p.pos += 1 + else: + self._do_warn_unknown_char(ch) + p.latex += self._do_unknown_char(ch) + p.pos += 1 + + return p.latex + + def _check_do_skip_ascii(self, s, p): + if ord(s[p.pos]) < 127: + # skip, we only want to convert non-ascii chars + p.latex += s[p.pos] + p.pos += 1 + return True + return False + + def _apply_rule_dict(self, ruledict, rule, s, p): + o = ord(s[p.pos]) + if o in ruledict: + self._apply_replacement(p, ruledict[o], 1, rule) + return True + return None + + def _apply_rule_regex(self, ruleregexes, rule, s, p): + for regex, repl in ruleregexes: + m = regex.match(s, p.pos) + if m is not None: + if callable(repl): + replstr = repl(m) + else: + replstr = m.expand(repl) + self._apply_replacement(p, replstr, m.end() - m.start(), rule) + return True + return None + + def _apply_rule_callable(self, rulecallable, rule, s, p): + res = rulecallable(s, p.pos) + if res is None: + return None + (consumed, repl) = res + self._apply_replacement(p, repl, consumed, rule) + return True + + def _apply_replacement(self, p, repl, numchars, ruleobj): + # check for possible replacement latex protection, like braces. + + protect_fn = self._apply_protection + + # maybe the rule object has overridden the replacement_latex_protection to use. + if ruleobj.replacement_latex_protection is not None: + protect_fn = self._get_replacement_latex_fn( + ruleobj.replacement_latex_protection + ) + + repl = protect_fn(repl) + p.latex += repl + p.pos += numchars + + def _apply_protection_none(self, repl): + # no protection + return repl + + def _apply_protection_braces(self, repl): + k = repl.rfind("\\") + if k >= 0 and repl[k + 1 :].isalpha(): + # has dangling named macro, apply protection. + return "{" + repl + "}" + return repl + + def _apply_protection_braces_almost_all(self, repl): + if repl[0:1] == "\\": + return "{" + repl + "}" + return repl + + def _apply_protection_braces_all(self, repl): + return "{" + repl + "}" + + def _apply_protection_braces_after_macro(self, repl): + k = repl.rfind("\\") + if k >= 0 and repl[k + 1 :].isalpha(): + # has dangling named macro, apply protection. + return repl + "{}" + return repl + + # policies for "bad chars": + def _do_unknown_char_keep(self, ch): + return ch + + def _do_unknown_char_replace(self, ch): + return r"{\bfseries ?}" + + def _do_unknown_char_ignore(self, ch): + return "" + + def _do_unknown_char_fail(self, ch): + raise ValueError( + "No known latex representation for character: U+%04X - ‘%s’" % (ord(ch), ch) + ) + + def _do_unknown_char_unihex(self, ch): + return r"\ensuremath{\langle}\texttt{U+%04X}\ensuremath{\rangle}" % (ord(ch)) + + def _do_warn_unknown_char(self, ch): + logger.warning( + "No known latex representation for character: U+%04X - ‘%s’", ord(ch), ch + ) diff --git a/good_ai/src/sus/external/pylatexenc/latexwalker/__init__.py b/good_ai/src/sus/external/pylatexenc/latexwalker/__init__.py new file mode 100644 index 0000000..0cf574a --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexwalker/__init__.py @@ -0,0 +1,2873 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +r''' +The ``latexwalker`` module provides a simple API for parsing LaTeX snippets, +and representing the contents using a data structure based on node classes. + +LatexWalker will understand the syntax of most common macros. However, +``latexwalker`` is NOT a replacement for a full LaTeX engine. (Originally, +``latexwalker`` was designed to extract useful text for indexing for text +database searches of LaTeX content.) + +Simple example usage:: + + >>> from pylatexenc.latexwalker import LatexWalker, LatexEnvironmentNode + >>> w = LatexWalker(r""" + ... \textbf{Hi there!} Here is \emph{a list}: + ... \begin{enumerate}[label=(i)] + ... \item One + ... \item Two + ... \end{enumerate} + ... and $x$ is a variable. + ... """) + >>> (nodelist, pos, len_) = w.get_latex_nodes(pos=0) + >>> nodelist[0] + LatexCharsNode(pos=0, len=1, chars='\n') + >>> nodelist[1] + LatexMacroNode(pos=1, len=18, macroname='textbf', + nodeargd=ParsedMacroArgs(argnlist=[LatexGroupNode(pos=8, len=11, + nodelist=[LatexCharsNode(pos=9, len=9, chars='Hi there!')], + delimiters=('{', '}'))], argspec='{'), macro_post_space='') + >>> nodelist[5].isNodeType(LatexEnvironmentNode) + True + >>> nodelist[5].environmentname + 'enumerate' + >>> nodelist[5].nodeargd.argspec + '[' + >>> nodelist[5].nodeargd.argnlist + [LatexGroupNode(pos=60, len=11, nodelist=[LatexCharsNode(pos=61, len=9, + chars='label=(i)')], delimiters=('[', ']'))] + >>> nodelist[7].latex_verbatim() + '$x$' + +You can also use `latexwalker` directly in command-line, producing JSON or a +human-readable node tree:: + + $ echo '\textit{italic} text' | latexwalker --output-format=json + { + "nodelist": [ + { + "nodetype": "LatexMacroNode", + "pos": 0, + "len": 15, + "macroname": "textit", + [...] + + $ latexwalker --help + [...] + +The parser can be influenced by specifying a collection of known macros and +environments (the "latex context") that are specified using +:py:class:`pylatexenc.macrospec.MacroSpec` and +:py:class:`pylatexenc.macrospec.EnvironmentSpec` objects in a +:py:class:`pylatexenc.macrospec.LatexContextDb` object. See the doc of the +module :py:mod:`pylatexenc.macrospec` for more information. +''' + +from __future__ import print_function, unicode_literals + +import json +import logging +import re + +from .. import _util, macrospec + + +# Py3 +def unicode(string): + return string + + +_basestring = str +_str_from_unicode = lambda x: x +_unicode_from_str = lambda x: x + +logger = logging.getLogger(__name__) + + +def _maketuple(*args): + # for use with Python 2, where we don't have *args expansion in tuples and + # lists + return tuple(args) + + +class LatexWalkerError(Exception): + """ + Generic exception class raised by this module. + """ + + +class LatexWalkerParseError(LatexWalkerError): + """ + Represents an error while parsing LaTeX code. + + The following attributes are available if they were provided to the class + constructor: + + .. py:attribute:: msg + + The error message + + .. py:attribute:: s + + The string that was currently being parsed + + .. py:attribute:: pos + + The index in the string where the error occurred, starting at zero. + + .. py:attribute:: lineno + + The line number where the error occurred, starting at 1. + + .. py:attribute:: colno + + The column number where the error occurred in the line `lineno`, starting + at 1. + """ + + def __init__(self, msg, s=None, pos=None, lineno=None, colno=None): + self.input_source = None # attribute can be set to add to error msg display + self.msg = msg + self.s = s + self.pos = pos + self.lineno = lineno + self.colno = colno + self.open_contexts = [] + + super(LatexWalkerParseError, self).__init__(self._dispstr()) + + def _dispstr(self): + msg = self.msg + if self.input_source: + msg += " in {}".format(self.input_source) + disp = msg + " %s" % (self._fmt_pos(self.pos, self.lineno, self.colno)) + if self.open_contexts: + disp += "\nOpen LaTeX blocks:\n" + for context in reversed(self.open_contexts): + what, pos, lineno, colno = context + disp += "{empty:8}{loc:>10} {what}\n".format( + empty="", loc=self._fmt_pos(pos, lineno, colno), what=what + ) + return disp + + def _fmt_pos(self, pos, lineno, colno): + if lineno is not None: + if colno is not None: + return "@(%d,%d)" % (lineno, colno) + return "@%d" % (lineno) + return "@ char %d" % (pos) + + def __str__(self): + return self._dispstr() + + +class LatexWalkerEndOfStream(LatexWalkerError): + """ + Reached end of input stream (e.g., end of file). + """ + + def __init__(self, final_space=""): + super(LatexWalkerEndOfStream, self).__init__() + self.final_space = final_space + + +def get_default_latex_context_db(): + r""" + Return a :py:class:`pylatexenc.macrospec.LatexContextDb` instance + initialized with a collection of known macros and environments. + + TODO: document categories. + + If you want to add your own definitions, you should use the + :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()` + method. If you would like to override some definitions, use that method + with the argument `prepend=True`. See docs for + :py:meth:`pylatexenc.macrospec.LatexContextDb.add_context_category()`. + + If there are too many macro/environment definitions, or if there are some + irrelevant ones, you can always filter the returned database using + :py:meth:`pylatexenc.macrospec.LatexContextDb.filter_context()`. + + .. versionadded:: 2.0 + + The :py:class:`pylatexenc.macrospec.LatexContextDb` class as well as this + method, were all introduced in `pylatexenc 2.0`. + """ + db = macrospec.LatexContextDb() + + from ._defaultspecs import specs + + for cat, catspecs in specs: + db.add_context_category( + cat, + macros=catspecs["macros"], + environments=catspecs["environments"], + specials=catspecs["specials"], + ) + + return db + + +# provide an interface compatibile with pylatexenc 1.x +def MacrosDef(macname, optarg, numargs): + r""" + .. deprecated:: 2.0 + + Use :py:func:`pylatexenc.macrospec.std_macro` instead which does the same + thing, or invoke the :py:class:`~pylatexenc.macrospec.MacroSpec` class + directly (or a subclass). + + In `pylatexenc 1.x`, `MacrosDef` was a class. Since `pylatexenc 2.0`, + `MacrosDef` is a function which returns a + :py:class:`~pylatexenc.macrospec.MacroSpec` instance. In this way the + earlier idiom ``MacrosDef(...)`` still works in `pylatexenc 2`. The + field names of the constructed object might have changed since + `pylatexenc 1.x`, so you might have to adapt existing code if you were + accessing individual fields of `MacrosDef` objects. + + In the object returned by `MacrosDef()`, we provide the legacy attributes + `macname`, `optarg`, and `numargs`, so that existing code accessing those + properties can continue to work. + """ + _util.pylatexenc_deprecated_2( + "`pylatexenc.latexwalker.MacrosDef` is now obsolete. " + "It should still work in most use cases, but new code should use " + "`pylatexenc.macrospec.MacroSpec` instead." + ) + + m = macrospec.std_macro(macname, optarg, numargs) + # make accessible legacy attributes + m.macname = m.macroname + m.optarg = optarg + m.numargs = numargs + # also, make the macro args parser ignore any leading '*'-s to emulate + # pylatexenc 1.x behavior + m.args_parser._like_pylatexenc1x_ignore_leading_star = True + return m + + +default_macro_dict = _util.LazyDict( + generate_dict_fn=lambda: dict( + [(m.macroname, m) for m in get_default_latex_context_db().iter_macro_specs()] + ) +) +r""" +.. deprecated:: 2.0 + + Use :py:func:`get_default_latex_context_db()` instead, or create your own + :py:class:`pylatexenc.macrospec.LatexContextDb` object. + + +Provide an access to the default macro specs for `latexwalker` in a form +that is compatible with `pylatexenc 1.x`\ 's `default_macro_dict` module-level +dictionary. + +This is implemented using a custom lazy mutable mapping, which behaves just like +a regular dictionary but that loads the data only once the dictionary is +accessed. In this way the default latex specs into a python dictionary unless +they are actually queried or modified, and thus users of `pylatexenc 2.0` that +don't rely on the default macro/environment definitions shouldn't notice any +decrease in performance. +""" + + +# ------------------------------------------------ + + +class LatexToken(object): + r""" + Represents a token read from the LaTeX input. + + This is used internally by :py:class:`LatexWalker`'s methods. You probably + don't need to worry about individual tokens. Rather, you should use the + high-level functions provided by :py:class:`LatexWalker` (e.g., + :py:meth:`~LatexWalker.get_latex_nodes()`). So most likely, you can ignore + this class entirely. + + Instances of this class are what the method + :py:meth:`LatexWalker.get_token()` returns. See the doc of that function + for more information on how tokens are parsed. + + This is not the same thing as a LaTeX token, it's just a part of the input + which we treat in the same way (e.g. a bunch of content characters, a + comment, a macro, etc.) + + Information about the object is stored into the fields `tok` and `arg`. The + `tok` field is a string which identifies the type of the token. The `arg` + depends on what `tok` is, and describes the actual input. + + Additionally, this class stores information about the position of the token + in the input stream in the field `pos`. This `pos` is an integer which + corresponds to the index in the input string. The field `len` stores the + length of the token in the input string. This means that this token spans + in the input string from `pos` to `pos+len`. + + Leading whitespace before the token is not returned as a separate + 'char'-type token, but it is given in the `pre_space` field of the token + which follows. Pre-space may contain a newline, but not two consecutive + newlines. + + The `post_space` is only used for 'macro' and 'comment' tokens, and it + stores any spaces encountered after a macro, or the newline with any + following spaces that terminates a LaTeX comment. When we encounter two + consecutive newlines these are not included in `post_space`. + + The `tok` field may be one of: + + - 'char': raw character(s) which have no special LaTeX meaning and which + are part of the text content. + + The `arg` field contains the characters themselves. + + - 'macro': a macro invocation, but not ``\begin`` or ``\end`` + + The `arg` field contains the name of the macro, without the leading + backslash. + + - 'begin_environment': an invocation of ``\begin{environment}``. + + The `arg` field contains the name of the environment inside the braces. + + - 'end_environment': an invocation of ``\end{environment}``. + + The `arg` field contains the name of the environment inside the braces. + + - 'comment': a LaTeX comment delimited by a percent sign up to the end of + the line. + + The `arg` field contains the text in the comment line, not including the + percent sign nor the newline. + + - 'brace_open': an opening brace. This is usually a curly brace, and + sometimes also a square bracket. What is parsed as a brace depends on + the arguments to :py:meth:`~LatexWalker.get_token()`. + + The `arg` is a string which contains the relevant brace character. + + - 'brace_close': a closing brace. This is usually a curly brace, and + sometimes also a square bracket. What is parsed as a brace depends on + the arguments to :py:meth:`~LatexWalker.get_token()`. + + The `arg` is a string which contains the relevant brace character. + + - 'mathmode_inline': a delimiter which starts/ends inline math. This is + (e.g.) a single '$' character which is not part of a double '$$' + display environment delimiter. + + The `arg` is the string value of the delimiter in question ('$') + + - 'mathmode_display': a delimiter which starts/ends display math, e.g., + ``\[``. + + The `arg` is the string value of the delimiter in question (e.g., + ``\[`` or ``$$``) + + - 'specials': a character or character sequence that has a special + meaning in LaTeX. E.g., '~', '&', etc. + + The `arg` field is then the corresponding + :py:class:`~pylatexenc.macrospec.SpecialsSpec` instance. [The rationale + for setting `arg` to a `SpecialsSpec` instance, in contrast to the + behavior for macros and envrionments, is that macros and environments + are delimited directly by LaTeX syntax and are determined unambiguously + without any lookup in the latex context database. This is not the case + for specials.] + """ + + def __init__(self, tok, arg, pos, len, pre_space, post_space=""): + self.tok = tok + self.arg = arg + self.pos = pos + self.len = len + self.pre_space = pre_space + self.post_space = post_space + self._fields = ["tok", "arg", "pos", "len", "pre_space"] + if self.tok in ("macro", "comment"): + self._fields.append("post_space") + super(LatexToken, self).__init__() + + def __unicode__(self): + return _unicode_from_str(self.__str__()) + + def __repr__(self): + return ( + "LatexToken(" + + ", ".join(["%s=%r" % (k, getattr(self, k)) for k in self._fields]) + + ")" + ) + + def __str__(self): + return self.__repr__() + + def __eq__(self, other): + return all((getattr(self, f) == getattr(other, f) for f in self._fields)) + + # see https://docs.python.org/3/library/constants.html#NotImplemented + def __ne__(self, other): + return NotImplemented + + __hash__ = None + + +# ------------------------------------------------ + + +class LatexNode(object): + """ + Represents an abstract 'node' of the latex document. + + Use :py:meth:`nodeType()` to figure out what type of node this is, and + :py:meth:`isNodeType()` to test whether it is of a given type. + + You should use :py:meth:`LatexWalker.make_node()` to create nodes, so that + the latex walker has the opportunity to do some additional setting up. + + All nodes have the following attributes: + + .. py:attribute:: parsing_state + + The parsing state at the time this node was created. This object stores + additional context information for this node, such as whether or not this + node was parsed in a math mode block of LaTeX code. + + See also the :py:meth:`LatexWalker.make_parsing_state()` and the + `parsing_state` argument of :py:meth:`LatexWalker.get_latex_nodes()`. + + .. py:attribute:: pos + + The position in the parsed string that this node represents. The parsed + string can be recovered as `parsing_state.s`, see + :py:attr:`ParsingState.s`. + + .. py:attribute:: len + + How many characters in the parsed string this node represents, starting + at position `pos`. The parsed string can be recovered as + `parsing_state.s`, see :py:attr:`ParsingState.s`. + + .. versionadded:: 2.0 + + The attributes `parsing_state`, `pos` and `len` were added in + `pylatexenc 2.0`. + """ + + def __init__( + self, + _fields, + _redundant_fields=None, + parsing_state=None, + pos=None, + len=None, + **kwargs + ): + + # Important: subclasses must specify a list of fields they set in the + # `_fields` argument. They should only specify base (non-redundant) + # fields; if they have "redundant" fields, specify the additional fields + # in _redundant_fields=... + super(LatexNode, self).__init__(**kwargs) + + self.parsing_state = parsing_state + self.pos = pos + self.len = len + + self._fields = tuple(["pos", "len"] + list(_fields)) + if _redundant_fields is not None: + self._redundant_fields = tuple(list(self._fields) + list(_redundant_fields)) + else: + self._redundant_fields = self._fields + + def nodeType(self): + """ + Returns the class which corresponds to the type of this node. This is a + Python class object, that is one of + :py:class:`~pylatexenc.latexwalker.LatexCharsNode`, + :py:class:`~pylatexenc.latexwalker.LatexGroupNode`, etc. + """ + return LatexNode + + def isNodeType(self, t): + """ + Returns `True` if the current node is of the given type. The argument `t` + must be a Python class such as, + e.g. :py:class:`~pylatexenc.latexwalker.LatexGroupNode`. + """ + return isinstance(self, t) + + def latex_verbatim(self): + r""" + Return the chunk of LaTeX code that this node represents. + + This is a shorthand for ``node.parsing_state.s[node.pos:node.pos+node.len]``. + """ + if self.parsing_state is None: + raise TypeError( + "Can't use latex_verbatim() on node because we don't " + "have any parsing_state set" + ) + return self.parsing_state.s[self.pos : self.pos + self.len] + + def __eq__(self, other): + return ( + other is not None + and self.nodeType() == other.nodeType() + and other.parsing_state is self.parsing_state + and other.pos == self.pos + and other.len == self.len + and all((getattr(self, f) == getattr(other, f) for f in self._fields)) + ) + + # see https://docs.python.org/3/library/constants.html#NotImplemented + def __ne__(self, other): + return NotImplemented + + __hash__ = None + + def __unicode__(self): + return _unicode_from_str(self.__str__()) + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return ( + self.nodeType().__name__ + + "(" + + "parsing_state=, ".format(id(self.parsing_state)) + + ", ".join(["%s=%r" % (k, getattr(self, k)) for k in self._fields]) + + ")" + ) + + +class LatexCharsNode(LatexNode): + """ + A string of characters in the LaTeX document, without any special LaTeX + code. + + .. py:attribute:: chars + + The string of characters represented by this node. + """ + + def __init__(self, chars, **kwargs): + super(LatexCharsNode, self).__init__(_fields=("chars",), **kwargs) + self.chars = chars + + def nodeType(self): + return LatexCharsNode + + +class LatexGroupNode(LatexNode): + r""" + A LaTeX group delimited by braces, ``{like this}``. + + Note: in the case of an optional macro or environment argument, this node is + also used to represents a group delimited by square braces instead of curly + braces. + + .. py:attribute:: nodelist + + A list of nodes describing the contents of the LaTeX braced group. Each + item of the list is a :py:class:`LatexNode`. + + .. py:attribute:: delimiters + + A 2-item tuple that stores the delimiters for this group node. Usually + this is `('{', '}')`, except for optional macro arguments where this + might be for instance `('[', ']')`. + + .. versionadded:: 2.0 + + The `delimiters` field was added in `pylatexenc 2.0`. + """ + + def __init__(self, nodelist, **kwargs): + delimiters = kwargs.pop("delimiters", ("{", "}")) + super(LatexGroupNode, self).__init__( + _fields=( + "nodelist", + "delimiters", + ), + **kwargs + ) + self.nodelist = nodelist + self.delimiters = delimiters + + def nodeType(self): + return LatexGroupNode + + +class LatexCommentNode(LatexNode): + r""" + A LaTeX comment, delimited by a percent sign until the end of line. + + .. py:attribute:: comment + + The comment string, not including the '%' sign nor the following newline + + .. py:attribute:: comment_post_space + + The newline that terminated the comment possibly followed by spaces + (e.g., indentation spaces of the next line) + + """ + + def __init__(self, comment, **kwargs): + comment_post_space = kwargs.pop("comment_post_space", "") + + super(LatexCommentNode, self).__init__( + _fields=( + "comment", + "comment_post_space", + ), + **kwargs + ) + + self.comment = comment + self.comment_post_space = comment_post_space + + def nodeType(self): + return LatexCommentNode + + +class LatexMacroNode(LatexNode): + r""" + Represents a macro type node, e.g. ``\textbf`` + + .. py:attribute:: macroname + + The name of the macro (string), *without* the leading backslash. + + .. py:attribute:: nodeargd + + The :py:class:`pylatexenc.macrospec.ParsedMacroArgs` object that + represents the macro arguments. + + For macros that do not accept any argument, this is an empty + :py:class:`~pylatexenc.macrospec.ParsedMacroArgs` instance. The + attribute `nodeargd` can be `None` even for macros that accept arguments, + in the situation where :py:meth:`LatexWalker.get_latex_expression()` + encounters the macro when reading a single expression. + + Arguments must be declared in the latex context passed to the + :py:class:`LatexWalker` constructor, using a suitable + :py:class:`pylatexenc.macrospec.MacroSpec` object. Some known macros are + already declared in the default latex context. + + .. versionadded:: 2.0 + + The `nodeargd` attribute was introduced in `pylatexenc 2`. + + .. py:attribute:: macro_post_space + + Any spaces that were encountered immediately after the macro. + + The following attributes are obsolete since `pylatexenc 2.0`. + + .. py:attribute:: nodeoptarg + + .. deprecated:: 2.0 + + Macro arguments are stored in `nodeargd` in `pylatexenc 2`. Accessing + the argument `nodeoptarg` will still give a first optional argument + for standard latex macros, for backwards compatibility. + + If non-`None`, this corresponds to the optional argument of the macro. + + .. py:attribute:: nodeargs + + .. deprecated:: 2.0 + + Macro arguments are stored in `nodeargd` in pylatexenc 2. Accessing + the argument `nodeargs` will still provide a list of argument nodes + for standard latex macros, for backwards compatibility. + + A list of arguments to the macro. Each item in the list is a + :py:class:`LatexNode`. + """ + + def __init__(self, macroname, **kwargs): + nodeargd = kwargs.pop("nodeargd", macrospec.ParsedMacroArgs()) + macro_post_space = kwargs.pop("macro_post_space", "") + # legacy: + nodeoptarg = kwargs.pop("nodeoptarg", None) + nodeargs = kwargs.pop("nodeargs", []) + + super(LatexMacroNode, self).__init__( + _fields=("macroname", "nodeargd", "macro_post_space"), + _redundant_fields=("nodeoptarg", "nodeargs"), + **kwargs + ) + + self.macroname = macroname + self.nodeargd = nodeargd + self.macro_post_space = macro_post_space + # legacy: + self.nodeoptarg = nodeoptarg + self.nodeargs = nodeargs + + def nodeType(self): + return LatexMacroNode + + +class LatexEnvironmentNode(LatexNode): + r""" + A LaTeX Environment Node, i.e. ``\begin{something} ... \end{something}``. + + .. py:attribute:: environmentname + + The name of the environment ('itemize', 'equation', ...) + + .. py:attribute:: nodelist + + A list of :py:class:`LatexNode`'s that represent all the contents between + the ``\begin{...}`` instruction and the ``\end{...}`` instruction. + + .. py:attribute:: nodeargd + + The :py:class:`pylatexenc.macrospec.ParsedMacroArgs` object that + represents the arguments passed to the environment. These are arguments + that are present after the ``\begin{xxxxxx}`` command, as in + ``\begin{tabular}{ccc}`` or ``\begin{figure}[H]``. Arguments must be + declared in the latex context passed to the :py:class:`LatexWalker` + constructor, using a suitable + :py:class:`pylatexenc.macrospec.EnvironmentSpec` object. Some known + environments are already declared in the default latex context. + + .. versionadded:: 2.0 + + The `nodeargd` attribute was introduced in `pylatexenc 2`. + + The following attributes are available, but they are obsolete since + `pylatexenc 2.0`. + + .. py:attribute:: envname + + .. deprecated:: 2.0 + + This attribute was renamed `environmentname` for consistency with the + rest of the package. + + .. py:attribute:: optargs + + .. deprecated:: 2.0 + + Macro arguments are stored in `nodeargd` in `pylatexenc 2`. Accessing + the argument `optargs` will still give a list of initial optional + arguments for standard latex macros, for backwards compatibility. + + .. py:attribute:: args + + .. deprecated:: 2.0 + + Macro arguments are stored in `nodeargd` in `pylatexenc 2`. Accessing + the argument `args` will still give a list of curly-brace-delimited + arguments for standard latex macros, for backwards compatibility. + """ + + def __init__(self, environmentname, nodelist, **kwargs): + nodeargd = kwargs.pop("nodeargd", macrospec.ParsedMacroArgs()) + # legacy: + optargs = kwargs.pop("optargs", []) + args = kwargs.pop("args", []) + + super(LatexEnvironmentNode, self).__init__( + _fields=( + "environmentname", + "nodelist", + "nodeargd", + ), + _redundant_fields=( + "envname", + "optargs", + "args", + ), + **kwargs + ) + + self.environmentname = environmentname + self.nodelist = nodelist + self.nodeargd = nodeargd + # legacy: + self.envname = environmentname + self.optargs = optargs + self.args = args + + def nodeType(self): + return LatexEnvironmentNode + + +class LatexSpecialsNode(LatexNode): + r""" + Represents a specials type node, e.g. ``&`` or ``~`` + + .. py:attribute:: specials_chars + + The name of the specials (string), *without* the leading backslash. + + .. py:attribute:: nodeargd + + If the specials spec (cf. :py:class:`~pylatexenc.macrospec.SpecialsSpec`) + has `args_parser=None` then the attribute `nodeargd` is set to `None`. + If `args_parser` is specified in the spec, then the attribute `nodeargd` + is a :py:class:`pylatexenc.macrospec.ParsedMacroArgs` instance that + represents the arguments to the specials. + + The `nodeargd` attribute can also be `None` even if the specials expects + arguments, in the special situation where + :py:meth:`LatexWalker.get_latex_expression()` encounters this specials. + + Arguments must be declared in the latex context passed to the + :py:class:`LatexWalker` constructor, using a suitable + :py:class:`pylatexenc.macrospec.SpecialsSpec` object. Some known latex + specials are already declared in the default latex context. + + .. versionadded:: 2.0 + + Latex specials were introduced in `pylatexenc 2.0`. + """ + + def __init__(self, specials_chars, **kwargs): + nodeargd = kwargs.pop("nodeargd", None) + + super(LatexSpecialsNode, self).__init__( + _fields=("specials_chars", "nodeargd"), **kwargs + ) + + self.specials_chars = specials_chars + self.nodeargd = nodeargd + + def nodeType(self): + return LatexSpecialsNode + + +class LatexMathNode(LatexNode): + r""" + A Math node type. + + Note that currently only 'inline' math environments are detected. + + .. py:attribute:: displaytype + + Either 'inline' or 'display', to indicate an inline math block or a + display math block. (Note that math environments such as + ``\begin{equation}...\end{equation}``, are reported as + :py:class:`LatexEnvironmentNode`'s, and not as + :py:class:`LatexMathNode`'s.) + + .. py:attribute:: delimiters + + A 2-item tuple containing the begin and end delimiters used to delimit + this math mode section. + + .. versionadded:: 2.0 + + The `delimiters` attribute was introduced in `pylatexenc 2`. + + .. py:attribute:: nodelist + + The contents of the environment, given as a list of + :py:class:`LatexNode`'s. + """ + + def __init__(self, displaytype, nodelist=[], **kwargs): + delimiters = kwargs.pop("delimiters", (None, None)) + + super(LatexMathNode, self).__init__( + _fields=("displaytype", "nodelist", "delimiters"), **kwargs + ) + + self.displaytype = displaytype + self.nodelist = nodelist + self.delimiters = delimiters + + def nodeType(self): + return LatexMathNode + + +# ------------------------------------------------------------------------------ + + +class _PushPropOverride(object): + def __init__(self, obj, propname, new_value): + super(_PushPropOverride, self).__init__() + self.obj = obj + self.propname = propname + self.new_value = new_value + + def __enter__(self): + if self.new_value is not None: + self.initval = getattr(self.obj, self.propname) + setattr(self.obj, self.propname, self.new_value) + return self + + def __exit__(self, type, value, traceback): + # clean-up + if self.new_value is not None: + setattr(self.obj, self.propname, self.initval) + + +class ParsingState(object): + r""" + Stores some information about the current parsing state, such as whether we + are currently in a math mode block. + + One of the ideas of `pylatexenc` is to make the parsing of LaTeX code mostly + state-independent mark-up parsing (in contrast to a full TeX engine, whose + state constantly changes and whose parsing behavior is altered dynamically + while parsing). However a minimal state of the context might come in handy + sometimes. Perhaps some macros or specials should behave differently in + math mode than in text mode. + + This class also stores some essential information that is associated with + :py:class:`LatexNode`\ 's and which provides a context to better understand + the node structure. For instance, we store the original parsed string, and + each node refers to which part of the string they represent. + + .. py:attribute:: s + + The string that is parsed by the :py:class:`LatexWalker` + + .. py:attribute:: latex_context + + The latex context (with macros/environments specifications) that was used + when parsing the string `s`. This is a + :py:class:`pylatexenc.macrospec.LatexContextDb` object. + + .. py:attribute:: in_math_mode + + Whether or not we are in a math mode chunk of LaTeX (True or False). + This can be inline or display, and can be caused by an equation + environment. + + .. py:attribute:: math_mode_delimiter + + Information about the kind of math mode we are currently in, if + `in_math_mode` is `True`. This is a string which can be set to aid the + parser. The parser sets this field to the math mode delimiter that + initiated the math mode (one of ``'$'``, ``'$$'``, ``r'\('``, ``r'\)'``). + For user-initiated math modes (e.g. by a custom environment definition), + you can set this string to any custom value EXCEPT any of the core math + mode delimiters listed above. + + .. note:: The tokenizer/parser relies on the value of the + `math_mode_delimiter` attribute to disambiguate two consecutive + dollar signs ``...$$...`` into either a display math mode + delimiter or two inline math mode delimiters (as in + ``$a$$b$``). You should only set `math_mode_delimiter='$'` if + you know what you're doing. + + .. versionadded:: 2.0 + + This class was introduced in version 2.0. + + .. versionadded:: 2.7 + + The attribute `math_mode_delimiter` was introduced in version 2.7. + + .. versionchanged:: 2.7 + + All arguments must now be specified as keyword arguments as of version + 2.7. + """ + + def __init__(self, **kwargs): + super(ParsingState, self).__init__() + self.s = None + self.latex_context = None + self.in_math_mode = False + self.math_mode_delimiter = None + self._fields = ( + "s", + "latex_context", + "in_math_mode", + "math_mode_delimiter", + ) + + do_sanitize = kwargs.pop("_do_sanitize", True) + + self._set_fields(kwargs, do_sanitize=do_sanitize) + + def sub_context(self, **kwargs): + r""" + Return a new :py:class:`ParsingState` instance that is a copy of the current + parsing state, but where the given properties keys have been set to the + corresponding values (given as keyword arguments). + + This makes it easy to create a sub-context in a given parser. For + instance, if we enter math mode, we might write:: + + parsing_state_inner = parsing_state.sub_context(in_math_mode=True) + + If no arguments are provided, this returns a copy of the present parsing + context object. + """ + p = self.__class__(_do_sanitize=False, **self.get_fields()) + + p._set_fields(kwargs) + + return p + + def get_fields(self): + r""" + Returns the fields and values associated with this `ParsingState` as a + dictionary. + """ + return dict([(f, getattr(self, f)) for f in self._fields]) + + def _set_fields(self, kwargs, do_sanitize=True): + + for k, v in kwargs.items(): + if k not in self._fields: + raise ValueError("Invalid field for ParsingState: {}={!r}".format(k, v)) + setattr(self, k, v) + + if do_sanitize: + # Do some sanitization. If we set in_math_mode=False, then we should + # clear any math_mode_delimiter. + self._sanitize(given_fields=kwargs) + + def _sanitize(self, given_fields): + """ + Sanitize the parsing state. E.g., clear any `math_mode_delimiter` if + `in_math_mode` is `False`. + + The argument `given_fields` is what fields the user required to set; + this is used to generate warnings if incompatible field configurations + were explicitly required to be set. + """ + if not self.in_math_mode and self.math_mode_delimiter: + self.math_mode_delimiter = None + if "math_mode_delimiter" in given_fields: + logger.warning( + "ParsingState: You set math_mode_delimiter=%r but " + "in_math_mode is False", + self.math_mode_delimiter, + ) + + +# ------------------------------------------------------------------------------ + + +class LatexWalker(object): + r""" + A parser which walks through an input stream, parsing it as LaTeX markup. + + Arguments: + + - `s`: the string to parse as LaTeX code + + - `latex_context`: a :py:class:`pylatexenc.macrospec.LatexContextDb` + object that provides macro and environment specifications with + instructions on how to parse arguments, etc. If you don't specify this + argument, or if you specify `None`, then the default database is used. + The default database is obtained with + :py:func:`get_default_latex_context_db()`. + + .. versionadded:: 2.0 + + This `latex_context` argument was introduced in version 2.0. + + Additional keyword arguments are flags which influence the parsing. + Accepted flags are: + + - `tolerant_parsing=True|False` If set to `True`, then the parser + generally ignores syntax errors rather than raising an exception. + + - `strict_braces=True|False` This option refers specifically to reading a + encountering a closing brace when an expression is needed. You + generally won't need to specify this flag, use `tolerant_parsing` + instead. + + The methods provided in this class perform various parsing of the given + string `s`. These methods typically accept a `pos` parameter, which must be + an integer, which defines the position in the string `s` to start parsing. + + These methods, unless otherwise documented, return a tuple `(node, pos, + len)`, where node is a :py:class:`LatexNode` describing the parsed content, + `pos` is the position at which the LaTeX element of iterest was encountered, + and `len` is the length of the string that is considered to be part of the + `node`. That is, the position in the string that is immediately after the + node is `pos+len`. + + The following obsolete flag is accepted by the constructor for backwards + compatibility with `pylatexenc 1.x`: + + - `macro_dict`: This argument is kept for compatibility with `pylatexenc + 1.x`. This is a dictionary of known LaTeX macro specifications. If + specified, this should be a dictionary where the keys are macro names + and values are :py:class:`pylatexenc.macrospec.MacroSpec` instances, as + returned for instance by the `pylatexenc 1.x`-emulating function + :py:func:`MacrosDef`. If you specify this argument, you cannot provide + a custom `latex_context`. This argument is superseded by the + `latex_context` argument. Furthermore, if you specify this argument, no + specials are parsed so that the behavior closer to `pylatexenc 1.x`. + + .. deprecated:: 2.0 + + The `macro_dict` argument has been replaced by the much more powerful + `latex_context` argument which allows you to further provide + environment specifications, etc. + + - `keep_inline_math=True|False`: Obsolete option. In `pylatexenc 1.x`, + this option triggered a weird behavior especially since there is a + similarly named option in + :py:class:`pylatexenc.latex2text.LatexNodes2Text` with a different + meaning. [See `Issue #14 + `_.] You should now + only use the option `math_mode=` in + :py:class:`pylatexenc.latex2text.LatexNodes2Text`. + + .. deprecated:: 2.0 + + This option is ignored starting from `pylatexenc 2`. Instead, you + should set the option `math_mode=` accordingly in + :py:class:`pylatexenc.latex2text.LatexNodes2Text`. + + + .. py:attribute:: s + + The string that is being parsed. + + Do NOT modify this attribute. + """ + + def __init__(self, s, latex_context=None, **kwargs): + + self.s = s + + # will be determined lazily automatically by pos_to_lineno_colno(...) + self._line_no_calc = None + + self.debug_nodes = False + + if latex_context is None: + if "macro_dict" in kwargs: + # LEGACY -- build a latex context using the given macro_dict + _util.pylatexenc_deprecated_2( + "The `macro_dict=...` option in LatexWalker() is obsolete since " + "pylatexenc 2. It'll still work, but please consider using instead " + "the more versatile option `latex_context=...`." + ) + + macro_dict = kwargs.pop("macro_dict", None) + + default_latex_context = get_default_latex_context_db() + + latex_context = default_latex_context.filter_context( + keep_which=["environments"], # no specials + ) + latex_context.add_context_category( + "custom", + macro_dict.values(), + default_latex_context.iter_environment_specs(), + ) + + else: + # default -- use default + latex_context = get_default_latex_context_db() + + else: + # make sure the user didn't also provide a macro_dict= argument + if "macro_dict" in kwargs: + raise TypeError( + "Cannot specify both `latex_context=` and `macro_dict=` arguments" + ) + + # We don't store the latex_context in an attribute, because we always + # access it via the current parsing_state + + self.default_parsing_state = ParsingState( + s=self.s, + latex_context=latex_context, + ) + + # + # now parsing flags: + # + self.tolerant_parsing = kwargs.pop("tolerant_parsing", True) + self.strict_braces = kwargs.pop("strict_braces", False) + + if "keep_inline_math" in kwargs: + _util.pylatexenc_deprecated_2( + "The keep_inline_math=... option in LatexWalker() has no effect " + "in pylatexenc 2. Please consider using the more versatile option " + "math_mode=... in LatexNodes2Text() instead." + ) + del kwargs["keep_inline_math"] + + if kwargs: + # any flags left which we haven't recognized + logger.warning( + "LatexWalker(): Unknown flag(s) encountered: %r", kwargs.keys() + ) + + super(LatexWalker, self).__init__() + + def make_parsing_state(self, **kwargs): + r""" + Return a new parsing state object that corresponds to the current string + that we are parsing (`s` provided to the constructor) and the current + latex context (`latex_context` provided to the constructor). + + If no arguments are provided, this returns the default parsing state. + + If keyword arguments are provided, then they can override fields from + the default parsing state. For instance, if we enter math mode, you + might use:: + + parsing_state_mathmode = \ + my_latex_walker.make_parsing_state(in_math_mode=True) + """ + return self.default_parsing_state.sub_context(**kwargs) + + def parse_flags(self): + """ + The parse flags currently set on this object. Returns a dictionary with + keys 'keep_inline_math', 'tolerant_parsing' and 'strict_braces'. + + .. deprecated:: 2.0 + + The 'keep_inline_math' key is always set to `None` starting in + `pylatexenc 2` and might be removed entirely in future versions. + """ + return { + "tolerant_parsing": self.tolerant_parsing, + "strict_braces": self.strict_braces, + # compatibility with pylatexenc 1.x + "keep_inline_math": None, + } + + def _report_ignore_parse_error(self, exc): + logger.info("Ignoring parse error (tolerant parsing mode): %s", exc) + + def get_token( + self, + pos, + include_brace_chars=None, + environments=True, + keep_inline_math=None, + parsing_state=None, + **kwargs + ): + r""" + Parses the latex content given to the constructor (and stored in `self.s`), + starting at position `pos`, to parse a single "token", as defined by + :py:class:`LatexToken`. + + Parse the token in the stream pointed to at position `pos`. + + For tokens of type 'char', usually a single character is returned. The + only exception is at paragraph boundaries, where a single 'char'-type + token has argument '\\n\\n'. + + Returns a :py:class:`LatexToken`. Raises + :py:exc:`LatexWalkerEndOfStream` if end of stream reached. + + The argument `include_brace_chars=` allows to specify additional pairs + of single characters which should be considered as braces (i.e., of + 'brace_open' and 'brace_close' token types). It should be a list of + 2-item tuples, for instance ``[('[', ']'), ('<', '>')]``. The pair + `('{', '}')` is always considered as braces. The delimiters may not + have more than one character each. + + If `environments=False`, then ``\begin`` and ``\end`` tokens count as + regular 'macro' tokens (see :py:class:`LatexToken`); otherwise (the + default) they are considered as the token types 'begin_environment' and + 'end_environment'. + + The parsing of the tokens might be influcenced by the `parsing_state` (a + :py:class:`ParsingState` instance). Currently, the only influence this + has is that some latex specials are parsed differently if in math mode. + See doc for :py:class:`ParsingState`. If `parsing_state` is `None`, the + default parsing state returned by :py:meth:`make_parsing_state()` is + used. + + .. deprecated:: 2.0 + + The flag `keep_inline_math` is only accepted for compatibiltiy with + earlier versions of `pylatexenc`, but it has no effect starting in + `pylatexenc 2`. See the :py:class:`LatexWalker` class doc. + + .. deprecated:: 2.0 + + If `brackets_are_chars=False`, then square bracket characters count + as 'brace_open' and 'brace_close' token types (see + :py:class:`LatexToken`); otherwise (the default) they are considered + just like other normal characters. + + .. versionadded:: 2.0 + + The `parsing_state` argument was introduced in version 2.0. + """ + + if parsing_state is None: + parsing_state = self.make_parsing_state() # get default parsing state + + brace_chars = [("{", "}")] + + if include_brace_chars: + brace_chars += include_brace_chars + + if "brackets_are_chars" in kwargs: + if not kwargs.pop("brackets_are_chars"): + brace_chars += [("[", "]")] + + s = self.s # shorthand + + space = "" # space that we gobble up before token + + # + # In tolerant parsing mode, this method should not raise + # LatexWalkerParseError. Instead, it should return whatever token (at + # the worst case, a placeholder chars token) it can to help the caller + # recover from errors. + # + # This is because we want to recover from errors as soon as possible. + # For instance a macro argument parser might rely on calls to + # get_token() to parse its command arguments (say check for a starred + # command); if an exception is raised then it will bubble up and make it + # harder to keep the macro in some meaningful way. We could have + # required instead to guard each call to get_token with a try/except + # block but it feels better to keep the same philosophy as internal + # calls to get_latex_expression(), etc., which simply return whatever + # they can instead of raising exceptions in tolerant parsing mode. + # + def _token_parse_error(msg, len, placeholder): + e = LatexWalkerParseError( + s=s, pos=pos, msg=msg, **self.pos_to_lineno_colno(pos, as_dict=True) + ) + if self.tolerant_parsing: + self._report_ignore_parse_error(e) + return None, LatexToken( + tok="char", arg=placeholder, pos=pos, len=len, pre_space=space + ) + return e, None + + while pos < len(s) and s[pos].isspace(): + space += s[pos] + pos += 1 + if space.endswith("\n\n"): # two \n's indicate new paragraph. + return LatexToken( + tok="char", arg="\n\n", pos=pos - 2, len=2, pre_space=space[:-2] + ) + + if pos >= len(s): + raise LatexWalkerEndOfStream(final_space=space) + + if s[pos] == "\\": + # escape sequence + if pos + 1 >= len(s): + raise LatexWalkerEndOfStream() + macro = s[pos + 1] # next char is necessarily part of macro + # following chars part of macro only if all are alphabetical + isalphamacro = False + i = 2 + if s[pos + 1].isalpha(): + isalphamacro = True + while pos + i < len(s) and s[pos + i].isalpha(): + macro += s[pos + i] + i += 1 + + # special treatment for \( ... \) and \[ ... \] -- "macros" for + # inline/display math modes + if macro in ["[", "]"]: + return LatexToken( + tok="mathmode_display", + arg="\\" + macro, + pos=pos, + len=i, + pre_space=space, + ) + if macro in ["(", ")"]: + return LatexToken( + tok="mathmode_inline", + arg="\\" + macro, + pos=pos, + len=i, + pre_space=space, + ) + + # see if we have a begin/end environment + if environments and macro in ["begin", "end"]: + # \begin{environment} or \end{environment} + envmatch = re.match(r"^\s*\{([\w* ._-]+)\}", s[pos + i :]) + if envmatch is None: + e, t = _token_parse_error( + msg=r"Bad \{} macro: expected {{environmentname}}".format( + macro + ), + len=i, + placeholder="\\" + macro, + ) + if e: + raise e + return t + + return LatexToken( + tok=( + "begin_environment" if macro == "begin" else "end_environment" + ), + arg=envmatch.group(1), + pos=pos, + len=i + envmatch.end(), # !!: envmatch.end() counts from pos+i + pre_space=space, + ) + + # get the following whitespace, and store it in the macro's post_space + post_space = "" + if isalphamacro: + # important, LaTeX does not consume space after non-alpha macros, like \& + while pos + i < len(s) and s[pos + i].isspace(): + post_space += s[pos + i] + i += 1 + if post_space.endswith("\n\n"): + # if two \n's are encountered this signals a new + # paragraph, so do not include them as part of the + # macro's post_space. + post_space = post_space[:-2] + i -= 2 + break + + return LatexToken( + tok="macro", + arg=macro, + pos=pos, + len=i, + pre_space=space, + post_space=post_space, + ) + + if s[pos] == "%": + # latex comment + m = re.compile(r"(\n|\r|\n\r)(?P\s*)").search(s, pos) + mlen = None + if m is not None: + if m.group("extraspace").startswith( + ( + "\n", + "\r", + "\n\r", + ) + ): + # special case where there is a \n immediately following the + # first one -- this is a new paragraph + arglen = m.start() - pos + mlen = m.start() - pos + mspace = "" + else: + arglen = m.start() - pos + mlen = m.end() - pos + mspace = m.group() + else: + arglen = len(s) - pos # [ ==len(s[pos:]) ] + mlen = arglen + mspace = "" + return LatexToken( + tok="comment", + arg=s[pos + 1 : pos + arglen], + pos=pos, + len=mlen, + pre_space=space, + post_space=mspace, + ) + + # see https://stackoverflow.com/a/19343/1694896 + openbracechars, closebracechars = zip(*brace_chars) + + if s[pos] in openbracechars: + return LatexToken( + tok="brace_open", arg=s[pos], pos=pos, len=1, pre_space=space + ) + + if s[pos] in closebracechars: + return LatexToken( + tok="brace_close", arg=s[pos], pos=pos, len=1, pre_space=space + ) + + # check for math-mode dollar signs. Using python syntax + # "string.startswith(pattern, pos)" + if s.startswith("$$", pos): + # if we are in an open '$'-delimited math mode, we need to parse $$ + # as two single $'s (issue #43) + if not ( + parsing_state.in_math_mode and parsing_state.math_mode_delimiter == "$" + ): + return LatexToken( + tok="mathmode_display", arg="$$", pos=pos, len=2, pre_space=space + ) + if s.startswith("$", pos): + return LatexToken( + tok="mathmode_inline", arg="$", pos=pos, len=1, pre_space=space + ) + + sspec = parsing_state.latex_context.test_for_specials( + s, pos, parsing_state=parsing_state + ) + if sspec is not None: + return LatexToken( + tok="specials", + arg=sspec, + pos=pos, + len=len(sspec.specials_chars), + pre_space=space, + ) + + # otherwise, the token is a normal 'char' type. + + return LatexToken(tok="char", arg=s[pos], pos=pos, len=1, pre_space=space) + + def make_node(self, node_class, **kwargs): + r""" + Create and return a node of type `node_class` which holds a representation + of the latex code at position `pos` and of length `len` in the parsed + string. + + The node class should be a :py:class:`LatexNode` subclass. Keyword + arguments are supplied directly to the constructor of the node class. + + Mandatory keyword-only arguments are 'pos', 'len', and 'parsing_state'. + + All nodes produced by :py:meth:`get_latex_nodes()` and friends use this + method to create node classes. + + .. versionadded:: 2.0 + + This method was introduced in `pylatexenc 2.0`. + """ + # mandatory keyword-only arguments: + pos, len, parsing_state = ( + kwargs.pop("pos"), + kwargs.pop("len"), + kwargs.pop("parsing_state"), + ) + + node = node_class(pos=pos, len=len, parsing_state=parsing_state, **kwargs) + if self.debug_nodes: + logger.debug("New node: %r", node) + return node + + def _mknodeposlen(self, nclass, parsing_state, pos, len, **kwargs): + return ( + self.make_node( + nclass, parsing_state=parsing_state, pos=pos, len=len, **kwargs + ), + pos, + len, + ) + + def pos_to_lineno_colno(self, pos, as_dict=False): + r""" + Return the line and column number corresponding to the given `pos` in our + string `self.s`. + + The first time this function is called, line numbers are calculated for + the entire string. These are cached for future calls which are then + fast. + + Return a tuple `(lineno, colno)` giving line number and column number. + Line numbers start at 1 and column numbers start at zero, i.e., the + beginning of the document (`pos=0`) has line and column number `(1,0)`. + If `as_dict=True`, then a dictionary with keys 'lineno', 'colno' is + returned instead of a tuple. + """ + + if self._line_no_calc is None: + self._line_no_calc = _util.LineNumbersCalculator(self.s) + + return self._line_no_calc.pos_to_lineno_colno(pos, as_dict=as_dict) + + def get_latex_expression(self, pos, strict_braces=None, parsing_state=None): + r""" + Parses the latex content given to the constructor (and stored in `self.s`), + starting at position `pos`, to parse a single LaTeX expression. + + Reads a latex expression, e.g. macro argument. This may be a single char, an escape + sequence, or a expression placed in braces. This is what TeX calls a "token" (and + not what we call a token... anyway). + + Parsing might be influenced by the `parsing_state`. See doc for + :py:class:`ParsingState`. If `parsing_state` is `None`, then the + default parsing state is used. + + Returns a tuple `(node, pos, len)`, where `pos` is the position of the + first char of the expression and `len` the length of the expression. + + .. versionadded:: 2.0 + + The `parsing_state` argument was introduced in version 2.0. + """ + + if parsing_state is None: + parsing_state = self.make_parsing_state() # get default parsing state + + with _PushPropOverride(self, "strict_braces", strict_braces): + + tok = self.get_token(pos, environments=False, parsing_state=parsing_state) + + if tok.tok == "macro": + if tok.arg == "end": + if not self.tolerant_parsing: + # error, we were expecting a single token + raise LatexWalkerParseError( + r"Expected expression, got \end", + self.s, + pos, + **self.pos_to_lineno_colno(pos, as_dict=True) + ) + else: + return self._mknodeposlen( + LatexCharsNode, + parsing_state=parsing_state, + chars="", + pos=tok.pos, + len=0, + ) + return self._mknodeposlen( + LatexMacroNode, + parsing_state=parsing_state, + macroname=tok.arg, + nodeargd=None, + macro_post_space=tok.post_space, + nodeoptarg=None, + nodeargs=None, + pos=tok.pos, + len=tok.len, + ) + if tok.tok == "specials": + return self._mknodeposlen( + LatexSpecialsNode, + parsing_state=parsing_state, + specials_chars=tok.arg.specials_chars, + nodeargd=None, + pos=tok.pos, + len=tok.len, + ) + if tok.tok == "comment": + return self.get_latex_expression( + tok.pos + tok.len, parsing_state=parsing_state + ) + if tok.tok == "brace_open": + return self.get_latex_braced_group(tok.pos, parsing_state=parsing_state) + if tok.tok == "brace_close": + # don't worry, stray closing braces are still reported (in + # get_latex_nodes()) if tolerant_parsing=False even if + # strict_braces=False. That's because we leave the brace in the + # input and it will be picked up when we read the next token. + if self.strict_braces and not self.tolerant_parsing: + raise LatexWalkerParseError( + "Expected expression, got closing brace '{}'".format(tok.arg), + self.s, + pos, + **self.pos_to_lineno_colno(pos, as_dict=True) + ) + return self._mknodeposlen( + LatexCharsNode, + parsing_state=parsing_state, + chars="", + pos=tok.pos, + len=0, + ) + if tok.tok == "char": + return self._mknodeposlen( + LatexCharsNode, + parsing_state=parsing_state, + chars=tok.arg, + pos=tok.pos, + len=tok.len, + ) + if tok.tok in ("mathmode_inline", "mathmode_display"): + # don't report a math mode token, treat as char or macro + if tok.arg.startswith("\\"): + return self._mknodeposlen( + LatexMacroNode, + parsing_state=parsing_state, + macroname=tok.arg, + nodeoptarg=None, + nodeargs=None, + macro_post_space=tok.post_space, + pos=tok.pos, + len=tok.len, + ) + else: + return self._mknodeposlen( + LatexCharsNode, + parsing_state=parsing_state, + chars=tok.arg, + pos=tok.pos, + len=tok.len, + ) + + raise LatexWalkerParseError( + "Unknown token type: {}".format(tok.tok), + self.s, + pos, + **self.pos_to_lineno_colno(pos, as_dict=True) + ) + + def get_latex_maybe_optional_arg(self, pos, parsing_state=None): + r""" + Parses the latex content given to the constructor (and stored in `self.s`), + starting at position `pos`, to attempt to parse an optional argument. + + Parsing might be influenced by the `parsing_state`. See doc for + :py:class:`ParsingState`. If `parsing_state` is `None`, the default + parsing state is used. + + Attempts to parse an optional argument. If this is successful, we return + a tuple `(node, pos, len)` if success where `node` is a + :py:class:`LatexGroupNode`. Otherwise, this method returns None. + + .. versionadded:: 2.0 + + The `parsing_state` argument was introduced in version 2.0. + """ + + if parsing_state is None: + parsing_state = self.make_parsing_state() # get default parsing state + + try: + tok = self.get_token( + pos, + include_brace_chars=[("[", "]")], + environments=False, + parsing_state=parsing_state, + ) + except LatexWalkerEndOfStream: + # we're at end of stream, simply report no optional arg and let + # parents re-detect end of stream when they call again get_token(). + # Added exception handler to fix issue #57 + return None + + if tok.tok == "brace_open" and tok.arg == "[": + return self.get_latex_braced_group( + pos, brace_type="[", parsing_state=parsing_state + ) + + return None + + def get_latex_braced_group(self, pos, brace_type="{", parsing_state=None): + r""" + Parses the latex content given to the constructor (and stored in `self.s`), + starting at position `pos`, to read a latex group delimited by braces. + + Reads a latex expression enclosed in braces ``{ ... }``. The first token of + `s[pos:]` must be an opening brace. + + Parsing might be influenced by the `parsing_state`. See doc for + :py:class:`ParsingState`. If `parsing_state` is `None`, the default + parsing state is used. + + Returns a tuple `(node, pos, len)`, where `node` is a + :py:class:`LatexGroupNode` instance, `pos` is the position of the first + char of the expression (which has to be an opening brace), and `len` is + the length of the group, including the closing brace (relative to the + starting position). + + The group must be delimited by the given `brace_type`. `brace_type` may + be one of ``{``, ``[``, ``(`` or ``<``, or a 2-item tuple of two + distinct single characters providing the opening and closing brace + chars (e.g., ``("<", ">")``). + + .. versionadded:: 2.0 + + The `parsing_state` argument was introduced in version 2.0. + """ + + if parsing_state is None: + parsing_state = self.make_parsing_state() # get default parsing state + + closing_brace = None + if brace_type == "{": + closing_brace = "}" + elif brace_type == "[": + closing_brace = "]" + elif brace_type == "(": + closing_brace = ")" + elif brace_type == "<": + closing_brace = ">" + elif len(brace_type) == 2: + brace_type, closing_brace = brace_type + else: + raise ValueError( + "Invalid brace type for get_latex_braced_group(): %s" % (brace_type) + ) + + include_brace_chars = None + if brace_type and brace_type != "{": + include_brace_chars = [(brace_type, closing_brace)] + + firsttok = self.get_token( + pos, include_brace_chars=include_brace_chars, parsing_state=parsing_state + ) + if firsttok.tok != "brace_open" or firsttok.arg != brace_type: + raise LatexWalkerParseError( + s=self.s, + pos=pos, + msg="get_latex_braced_group: not an opening brace/bracket: %s" + % (self.s[pos]), + **self.pos_to_lineno_colno(pos, as_dict=True) + ) + + (nodelist, npos, nlen) = self.get_latex_nodes( + firsttok.pos + firsttok.len, + stop_upon_closing_brace=(brace_type, closing_brace), + parsing_state=parsing_state, + ) + + return self._mknodeposlen( + LatexGroupNode, + nodelist=nodelist, + parsing_state=parsing_state, + delimiters=(brace_type, closing_brace), + pos=firsttok.pos, + len=npos + nlen - firsttok.pos, + ) + + def get_latex_environment(self, pos, environmentname=None, parsing_state=None): + r""" + Parses the latex content given to the constructor (and stored in `self.s`), + starting at position `pos`, to read a latex environment. + + Reads a latex expression enclosed in a + ``\begin{environment}...\end{environment}``. The first token in the + stream must be the ``\begin{environment}``. + + If `environmentname` is given and nonempty, then additionally a + :py:exc:`LatexWalkerParseError` is raised if the environment in the + input stream does not match the provided environment name. + + Arguments to the begin environment command are parsed according to the + corresponding specification in the given latex context `latex_context` + provided to the constructor. The environment name is looked up as a + "macro name" in the macro spec. + + Parsing might be influenced by the `parsing_state`. See doc for + :py:class:`ParsingState`. If `parsing_state` is `None`, the default + parsing state is used. + + Returns a tuple (node, pos, len) where node is a + :py:class:`LatexEnvironmentNode`. + + .. versionadded:: 2.0 + + The `parsing_state` argument was introduced in version 2.0. + """ + + if parsing_state is None: + parsing_state = self.make_parsing_state() # get default parsing state + + startpos = pos + + firsttok = self.get_token(pos, parsing_state=parsing_state) + if firsttok.tok != "begin_environment" or ( + environmentname is not None and firsttok.arg != environmentname + ): + raise LatexWalkerParseError( + s=self.s, + pos=pos, + msg=r"get_latex_environment: expected \begin{%s}: %s" + % ( + environmentname + if environmentname is not None + else "", + firsttok.arg, + ), + **self.pos_to_lineno_colno(pos, as_dict=True) + ) + if environmentname is None: + environmentname = firsttok.arg + + pos = firsttok.pos + firsttok.len + + env_spec = parsing_state.latex_context.get_environment_spec(environmentname) + if env_spec is None: + env_spec = macrospec.EnvironmentSpec("") + + # self = latex walker instance + try: + argsresult = env_spec.parse_args( + w=self, pos=pos, parsing_state=parsing_state + ) + except (LatexWalkerEndOfStream, LatexWalkerParseError) as e: + e = self._exchandle_parse_subexpression( + e, + firsttok, + 'arguments of environment "\\begin{{{}}}"'.format(environmentname), + ) + if e is not None: + raise e + argsresult = (None, pos, 0, {}) + + if len(argsresult) == 4: + (argd, apos, alen, adic) = argsresult + else: + (argd, apos, alen) = argsresult + adic = {} + + pos = apos + alen + + parsing_state_inner = adic.get("inner_parsing_state", parsing_state) + # parsing_state_inner = parsing_state + if env_spec.is_math_mode: + parsing_state_inner = parsing_state.sub_context( + in_math_mode=True, + math_mode_delimiter="{" + environmentname + "}", + ) + + (nodelist, npos, nlen) = self.get_latex_nodes( + pos, + stop_upon_end_environment=environmentname, + parsing_state=parsing_state_inner, + ) + + if argd is not None and argd.legacy_nodeoptarg_nodeargs: + legnodeoptarg = argd.legacy_nodeoptarg_nodeargs[0] + legnodeargs = argd.legacy_nodeoptarg_nodeargs[1] + else: + legnodeoptarg, legnodeargs = None, [] + + return self._mknodeposlen( + LatexEnvironmentNode, + parsing_state=parsing_state, + environmentname=environmentname, + nodelist=nodelist, + nodeargd=argd, + # legacy: + optargs=[legnodeoptarg], + args=legnodeargs, + pos=startpos, + len=npos + nlen - startpos, + ) + + def _exchandle_parse_subexpression(self, e, tok, what): + """ + (INTERNAL.) Handle an exception raised by a method that you called to parse + a macro arguments or another "sub-expression". Use as:: + + except (LatexWalkerEndOfStream, LatexWalkerParseError) as e: + e = self._exchandle_parse_subexpression(e, , "what this is about") + if e is not None: raise e + ... # do sth to recover from parse error in tolerant mode + + Use in an exception handler that captures both `LatexWalkerEndOfStream` + and `LatexWalkerParseError`. Returns what exception you should raise if + you got one of these while parsing, e.g., macro arguments. + """ + + if isinstance(e, LatexWalkerEndOfStream): + e = LatexWalkerParseError( + s=self.s, + pos=tok.pos, + msg="End of input while parsing {}".format(what), + **self.pos_to_lineno_colno(tok.pos, as_dict=True) + ) + + if getattr(e, "pos", None) is not None and e.lineno is None and e.colno is None: + e.lineno, e.colno = self.pos_to_lineno_colno(e.pos) + + e.open_contexts.append( + _maketuple("{}".format(what), tok.pos, *self.pos_to_lineno_colno(tok.pos)) + ) + + if self.tolerant_parsing: + self._report_ignore_parse_error(e) + return None + return e + + def get_latex_nodes( + self, + pos=0, + stop_upon_closing_brace=None, + stop_upon_end_environment=None, + stop_upon_closing_mathmode=None, + read_max_nodes=None, + parsing_state=None, + ): + r""" + Parses the latex content given to the constructor (and stored in `self.s`) + into a list of nodes. + + Returns a tuple `(nodelist, pos, len)` where: + + - `nodelist` is a list of :py:class:`LatexNode`\ 's representing the + parsed LaTeX code. + + - `pos` is the same as the `pos` given as argument; if there is + leading whitespace it is reported in `nodelist` using a + :py:class:`LatexCharsNode`. + + - `len` is the length of the parsed expression. If one of the + `stop_upon_...=` arguments are provided (cf below), then the `len` + includes the length of the token/expression that stopped the + parsing. + + If `stop_upon_closing_brace` is given and set to a character, then + parsing stops once the given closing brace is encountered (but not + inside a subgroup). The brace is given as a character, ']', '}', ')', + or '>'. Alternatively you may specify a 2-item tuple of two single + distinct characters representing the opening and closing brace chars. + The returned `len` includes the closing brace, but the closing brace is + not included in any of the nodes in the `nodelist`. + + If `stop_upon_end_environment` is provided, then parsing stops once the + given environment was closed. If there is an environment mismatch, then + a `LatexWalkerParseError` is raised except in tolerant parsing mode (see + :py:meth:`parse_flags()`). Again, the closing environment is included + in the length count but not the nodes. + + If `stop_upon_closing_mathmode` is specified, then the parsing stops + once the corresponding math mode (assumed already open) is closed. This + argument may take the values `None` (no particular request to stop at + any math mode token), or one of ``$``, ``$$``, ``\)`` or ``\]`` + indicating a closing math mode delimiter that we are expecting and at + which point parsing should stop. + + If the token '$' (respectively '$$') is encountered, it is interpreted + as the *beginning* of a new math mode chunk *unless* the argument + `stop_upon_closing_mathmode=...` has been set to '$' (respectively + '$$'). + + If `read_max_nodes` is non-`None`, then it should be set to an integer + specifying the maximum number of top-level nodes to read before + returning. (Top-level nodes means that macro arguments, environment or + group contents, etc., do not count towards `read_max_nodes`.) If + `None`, the entire input string will be parsed. + + .. note:: + + There are a few important differences between + ``get_latex_nodes(read_max_nodes=1)`` and ``get_latex_expression()``: + The former reads a logical node of the LaTeX document, which can be a + sequence of characters, a macro invocation with arguments, or an + entire environment, but the latter reads a single LaTeX "token" in + a similar way to how LaTeX parses macro arguments. + + For instance, if a macro is encountered, then + ``get_latex_nodes(read_max_nodes=1)`` will read and parse its + arguments, and include it in the corresponding + :py:class:`LatexMacroNode`, whereas ``get_latex_expression()`` will + return a minimal :py:class:`LatexMacroNode` with no arguments + regardless of the macro's argument specification. The same holds for + latex specials. For environments, + ``get_latex_nodes(read_max_nodes=1)`` will return the entire parsed + environment into a :py:class:`LatexEnvironmentNode`, whereas + ``get_latex_expression()`` will return a :py:class:`LatexMacroNode` + named 'begin' with no arguments. + + Parsing might be influenced by the `parsing_state`. See doc for + :py:class:`ParsingState`. If `parsing_state` is `None`, the default + parsing state is used. + + .. versionadded:: 2.0 + + The `parsing_state` argument was introduced in version 2.0. + """ + + if parsing_state is None: + parsing_state = self.make_parsing_state() # get default parsing state + + nodelist = [] + + include_brace_chars = None + opening_brace_for_stop_upon_closing_brace = None + if stop_upon_closing_brace: + if stop_upon_closing_brace == "}": + opening_brace_for_stop_upon_closing_brace = "{" + elif stop_upon_closing_brace == "]": + opening_brace_for_stop_upon_closing_brace = "[" + elif stop_upon_closing_brace == ")": + opening_brace_for_stop_upon_closing_brace = "(" + elif stop_upon_closing_brace == ">": + opening_brace_for_stop_upon_closing_brace = "<" + elif len(stop_upon_closing_brace) == 2: + ( + opening_brace_for_stop_upon_closing_brace, + stop_upon_closing_brace, + ) = stop_upon_closing_brace + + if stop_upon_closing_brace != "}": + include_brace_chars = [ + (opening_brace_for_stop_upon_closing_brace, stop_upon_closing_brace) + ] + + # consistency check + if stop_upon_closing_mathmode is not None and not parsing_state.in_math_mode: + logger.warning( + ( + "Call to LatexWalker.get_latex_nodes(stop_upon_closing_mathmode={!r}) " + "but parsing state has in_math_mode={!r}" + ).format( + stop_upon_closing_mathmode, + parsing_state.in_math_mode, + ) + ) + + # + # Man, I really need to rewrite this function properly. This is some + # pretty ugly sh*t. + # + + origpos = pos + + class PosPointer: + def __init__(self, pos, parsing_state, lastchars="", lastchars_pos=None): + self.pos = pos + self.parsing_state = parsing_state + self.lastchars = lastchars + self.lastchars_pos = lastchars_pos + + def push_lastchars(self, pos, chars): + self.lastchars += chars + if self.lastchars_pos is None: + self.lastchars_pos = pos + + def flush_lastchars(self): + res = self.lastchars_pos, self.lastchars + self.lastchars = "" + self.lastchars_pos = None + return res + + p = PosPointer(pos=pos, parsing_state=parsing_state) + + def do_read(nodelist, p): + r""" + Read a single token and process it, recursing into brace blocks and + environments etc if needed, and appending stuff to nodelist. + + Return True whenever we should stop trying to read more. (e.g. upon + reaching the a matched stop_upon_end_environment etc.) Can return + an exception instance to give more information than simply `True`. + """ + + try: + tok = self.get_token( + p.pos, + include_brace_chars=include_brace_chars, + parsing_state=p.parsing_state, + ) + except LatexWalkerEndOfStream as e: + if self.tolerant_parsing: + return e + raise # re-raise + except LatexWalkerParseError: + # get_token() should not raise parse errors in tolerant_parsing + # mode, because this can lead to infinite loops (#37) + assert not self.tolerant_parsing + raise # exception will be handled in outer loop + + p.pos = tok.pos + tok.len + + # def tok_to_pos_and_chars_from_ppos(tok): + # return tok.pos, self.s[p.pos, tok.pos+tok.len] + + # if it's a char, just append it to the stream of last characters. + if tok.tok == "char": + p.push_lastchars( + pos=(tok.pos - len(tok.pre_space)), chars=(tok.pre_space + tok.arg) + ) + return False + + # if it's not a char, push the last `p.lastchars` into the node list + # before we do anything else + if len(p.lastchars): + charspos, chars = p.flush_lastchars() + strnode = self.make_node( + LatexCharsNode, + parsing_state=p.parsing_state, + chars=chars + tok.pre_space, + pos=charspos, + len=tok.pos - charspos, + ) + nodelist.append(strnode) + if read_max_nodes and len(nodelist) >= read_max_nodes: + # adjust p.pos for return value of get_latex_nodes() + p.pos = tok.pos + return True + elif len(tok.pre_space): + # If we have pre_space, add a separate chars node that contains + # the spaces. We do this seperately, so that latex2text can + # ignore these groups by default to avoid too much space on the + # output. This allows latex2text to implement the + # `strict_latex_spaces=True` flag correctly. + spacestrnode = self.make_node( + LatexCharsNode, + parsing_state=p.parsing_state, + chars=tok.pre_space, + pos=tok.pos - len(tok.pre_space), + len=len(tok.pre_space), + ) + nodelist.append(spacestrnode) + if read_max_nodes and len(nodelist) >= read_max_nodes: + # adjust p.pos for return value of get_latex_nodes() + p.pos = tok.pos + return True + + # and see what the token is. + + if tok.tok == "brace_close": + # we've reached the end of the group. stop the parsing. + if tok.arg != stop_upon_closing_brace: + # p.push_lastchars(tok_to_pos_and_chars_from_ppos(tok)) + raise LatexWalkerParseError( + s=self.s, + pos=tok.pos, + msg="Unexpected mismatching closing brace: '%s'" % (tok.arg), + **self.pos_to_lineno_colno(tok.pos, as_dict=True) + ) + return True + + if tok.tok == "end_environment": + # we've reached the end of an environment. + if not stop_upon_end_environment: + # p.push_lastchars(tok_to_pos_and_chars_from_ppos(tok)) + raise LatexWalkerParseError( + s=self.s, + pos=tok.pos, + msg=("Unexpected closing environment: '{}'".format(tok.arg)), + **self.pos_to_lineno_colno(tok.pos, as_dict=True) + ) + elif tok.arg != stop_upon_end_environment: + # p.push_lastchars(tok_to_pos_and_chars_from_ppos(tok)) + raise LatexWalkerParseError( + s=self.s, + pos=tok.pos, + msg=( + "Unexpected mismatching closing environment: '{}', " + "was expecting '{}'".format( + tok.arg, stop_upon_end_environment + ) + ), + **self.pos_to_lineno_colno(tok.pos, as_dict=True) + ) + return True + + if tok.tok in ("mathmode_inline", "mathmode_display"): + # see if we need to stop at a math mode + if stop_upon_closing_mathmode is not None: + if tok.arg == stop_upon_closing_mathmode: + # all OK, found the closing mathmode. + return True + if tok.arg in [r"\)", r"\]"]: + # this is definitely a closing math-mode delimiter, so + # not a new math mode block. This is a parse error, + # because we need to match the given + # stop_upon_closing_mathmode mode. + + # p.push_lastchars(tok_to_pos_and_chars_from_ppos(tok)) + raise LatexWalkerParseError( + s=self.s, + pos=tok.pos, + msg="Mismatching closing math mode: '{}', expected '{}'".format( + tok.arg, + stop_upon_closing_mathmode, + ), + **self.pos_to_lineno_colno(tok.pos, as_dict=True) + ) + # all ok, this is a new math mode opening. Keep an assert + # in case we forget to include some math-mode delimiters in + # the future. + assert tok.arg in ["$", "$$", r"\(", r"\["] + elif tok.arg in [r"\)", r"\]"]: + # unexpected close-math-mode delimiter, but no + # stop_upon_closing_mathmode was specified. Parse error. + + # p.push_lastchars(tok_to_pos_and_chars_from_ppos(tok)) + raise LatexWalkerParseError( + s=self.s, + pos=tok.pos, + msg="Unexpected closing math mode: '{}'".format(tok.arg), + **self.pos_to_lineno_colno(tok.pos, as_dict=True) + ) + + # we have encountered a new math inline, parse the math expression + + corresponding_closing_mathmode = {r"\(": r"\)", r"\[": r"\]"}.get( + tok.arg, tok.arg + ) + displaytype = "inline" if tok.arg in [r"\(", "$"] else "display" + + parsing_state_inner = p.parsing_state.sub_context( + in_math_mode=True, math_mode_delimiter=tok.arg + ) + + try: + (mathinline_nodelist, mpos, mlen) = self.get_latex_nodes( + p.pos, + stop_upon_closing_mathmode=corresponding_closing_mathmode, + parsing_state=parsing_state_inner, + ) + except LatexWalkerParseError as e: + e.open_contexts.append( + _maketuple( + 'math mode "{}"'.format(tok.arg), + tok.pos, + *self.pos_to_lineno_colno(tok.pos) + ) + ) + raise + p.pos = mpos + mlen + + nodelist.append( + self.make_node( + LatexMathNode, + parsing_state=p.parsing_state, + displaytype=displaytype, + nodelist=mathinline_nodelist, + delimiters=(tok.arg, corresponding_closing_mathmode), + pos=tok.pos, + len=mpos + mlen - tok.pos, + ) + ) + if read_max_nodes and len(nodelist) >= read_max_nodes: + return True + return + + if tok.tok == "comment": + commentnode = self.make_node( + LatexCommentNode, + parsing_state=p.parsing_state, + comment=tok.arg, + comment_post_space=tok.post_space, + pos=tok.pos, + len=tok.len, + ) + nodelist.append(commentnode) + if read_max_nodes and len(nodelist) >= read_max_nodes: + return True + return + + if tok.tok == "brace_open": + # another braced group to read. + try: + (groupnode, bpos, blen) = self.get_latex_braced_group( + tok.pos, brace_type=tok.arg, parsing_state=p.parsing_state + ) + # except LatexWalkerEndOfStream as e: + # # shouldn't happen. + except LatexWalkerParseError as e: + e.open_contexts.append( + _maketuple( + "open brace", tok.pos, *self.pos_to_lineno_colno(tok.pos) + ) + ) + raise + + p.pos = bpos + blen + nodelist.append(groupnode) + if read_max_nodes and len(nodelist) >= read_max_nodes: + return True + return + + if tok.tok == "begin_environment": + # an environment to read. + try: + (envnode, epos, elen) = self.get_latex_environment( + tok.pos, environmentname=tok.arg, parsing_state=p.parsing_state + ) + except LatexWalkerParseError as e: + e.open_contexts.append( + _maketuple( + 'begin environment "{}"'.format(tok.arg), + tok.pos, + *self.pos_to_lineno_colno(tok.pos) + ) + ) + raise + p.pos = epos + elen + # add node and continue. + nodelist.append(envnode) + if read_max_nodes and len(nodelist) >= read_max_nodes: + return True + return + + if tok.tok == "macro": + # read a macro. see if it has arguments. + macroname = tok.arg + mspec = p.parsing_state.latex_context.get_macro_spec(macroname) + if mspec is None: + mspec = macrospec.MacroSpec("") + + try: + margsresult = mspec.parse_args( + w=self, pos=tok.pos + tok.len, parsing_state=p.parsing_state + ) + except (LatexWalkerEndOfStream, LatexWalkerParseError) as e: + e = self._exchandle_parse_subexpression( + e, tok, 'arguments of macro "{}"'.format(macroname) + ) + if e is not None: + raise e + margsresult = (None, tok.pos + tok.len, 0, {}) + + if len(margsresult) == 4: + (nodeargd, mapos, malen, mdic) = margsresult + else: + (nodeargd, mapos, malen) = margsresult + mdic = {} + + p.pos = mapos + malen + + if nodeargd is not None and nodeargd.legacy_nodeoptarg_nodeargs: + nodeoptarg = nodeargd.legacy_nodeoptarg_nodeargs[0] + nodeargs = nodeargd.legacy_nodeoptarg_nodeargs[1] + else: + nodeoptarg, nodeargs = None, [] + node = self.make_node( + LatexMacroNode, + parsing_state=p.parsing_state, + macroname=tok.arg, + nodeargd=nodeargd, + macro_post_space=tok.post_space, + # legacy data: + nodeoptarg=nodeoptarg, + nodeargs=nodeargs, + pos=tok.pos, + len=p.pos - tok.pos, + ) + nodelist.append(node) + + if "new_parsing_state" in mdic: + # modify current parsing state--- + p.parsing_state = mdic["new_parsing_state"] + + if read_max_nodes and len(nodelist) >= read_max_nodes: + return True + return None + + if tok.tok == "specials": + # read the specials. see if it expects/has arguments. + sspec = tok.arg + + p.pos = tok.pos + tok.len + nodeargd = None + + try: + res = sspec.parse_args( + w=self, pos=p.pos, parsing_state=p.parsing_state + ) + except (LatexWalkerEndOfStream, LatexWalkerParseError) as e: + e = self._exchandle_parse_subexpression( + e, + tok, + 'arguments of specials "{}"'.format(sspec.specials_chars), + ) + if e is not None: + raise e + res = (None, p.pos, 0, {}) + + if res is not None: + # specials expects arguments, read them + if len(res) == 4: + (nodeargd, mapos, malen, spdic) = res + else: + (nodeargd, mapos, malen) = res + spdic = {} + + p.pos = mapos + malen + + else: + spdic = {} + + node = self.make_node( + LatexSpecialsNode, + parsing_state=p.parsing_state, + specials_chars=sspec.specials_chars, + nodeargd=nodeargd, + pos=tok.pos, + len=p.pos - tok.pos, + ) + nodelist.append(node) + + if "new_parsing_state" in spdic: + # modify current parsing state--- + p.parsing_state = spdic["new_parsing_state"] + + if read_max_nodes and len(nodelist) >= read_max_nodes: + return True + return None + + raise LatexWalkerParseError( + s=self.s, + pos=p.pos, + msg="Unknown token: {!r}".format(tok), + **self.pos_to_lineno_colno(p.pos, as_dict=True) + ) + + while True: + try: + # might return boolean or Exception object + r_endnow = do_read(nodelist, p) + except LatexWalkerEndOfStream as e: + if ( + stop_upon_closing_brace + or stop_upon_end_environment + or stop_upon_closing_mathmode + ): + # unexpected eof + if stop_upon_closing_brace: + expecting = "'" + stop_upon_closing_brace + "'" + elif stop_upon_end_environment: + expecting = r"\end{" + stop_upon_end_environment + "}" + elif stop_upon_closing_mathmode: + expecting = "'" + stop_upon_closing_mathmode + "'" + e = LatexWalkerParseError( + s=self.s, + pos=p.pos, + msg="Unexpected end of stream, was expecting {}".format( + expecting + ), + **self.pos_to_lineno_colno(len(self.s), as_dict=True) + ) + if self.tolerant_parsing: + self._report_ignore_parse_error(e) + r_endnow = True + else: + raise e + else: + r_endnow = e + except LatexWalkerParseError as e: + if self.tolerant_parsing: + self._report_ignore_parse_error(e) + r_endnow = False + else: + raise + + if r_endnow: + + # add last chars and last space + if isinstance(r_endnow, LatexWalkerEndOfStream): + p.push_lastchars(pos=p.pos, chars=r_endnow.final_space) + p.pos += len(r_endnow.final_space) + + if p.lastchars: + charspos, chars = p.flush_lastchars() + strnode = self.make_node( + LatexCharsNode, + parsing_state=p.parsing_state, + chars=chars, + pos=charspos, + len=len(chars), + ) + nodelist.append(strnode) + return (nodelist, origpos, p.pos - origpos) + + # code never reaches here + + +# ------------------------------------------------------------------------------ + + +def get_token(s, pos, brackets_are_chars=True, environments=True, **parse_flags): + """ + Parse the next token in the stream. + + Returns a `LatexToken`. Raises `LatexWalkerEndOfStream` if end of stream reached. + + .. deprecated:: 1.0 + Please use :py:meth:`LatexWalker.get_token()` instead. + """ + return LatexWalker(s, **parse_flags).get_token( + pos=pos, brackets_are_chars=brackets_are_chars, environments=environments + ) + + +def get_latex_expression(s, pos, **parse_flags): + """ + Reads a latex expression, e.g. macro argument. This may be a single char, an escape + sequence, or a expression placed in braces. + + Returns a tuple `(, pos, len)`. `pos` is the first char of the + expression, and `len` is its length. + + .. deprecated:: 1.0 + Please use :py:meth:`LatexWalker.get_latex_expression()` instead. + """ + + return LatexWalker(s, **parse_flags).get_latex_expression(pos=pos) + + +def get_latex_maybe_optional_arg(s, pos, **parse_flags): + """ + Attempts to parse an optional argument. Returns a tuple `(groupnode, pos, len)` if + success, otherwise returns None. + + .. deprecated:: 1.0 + Please use :py:meth:`LatexWalker.get_latex_maybe_optional_arg()` instead. + """ + + return LatexWalker(s, **parse_flags).get_latex_maybe_optional_arg(pos=pos) + + +def get_latex_braced_group(s, pos, brace_type="{", **parse_flags): + """ + Reads a latex expression enclosed in braces {...}. The first token of `s[pos:]` must + be an opening brace. + + Returns a tuple `(node, pos, len)`. `pos` is the first char of the + expression (which has to be an opening brace), and `len` is its length, + including the closing brace. + + .. deprecated:: 1.0 + Please use :py:meth:`LatexWalker.get_latex_braced_group()` instead. + """ + + return LatexWalker(s, **parse_flags).get_latex_braced_group( + pos=pos, brace_type=brace_type + ) + + +def get_latex_environment(s, pos, environmentname=None, **parse_flags): + """ + Reads a latex expression enclosed in a \\begin{environment}...\\end{environment}. The first + token in the stream must be the \\begin{environment}. + + Returns a tuple (node, pos, len) with node being a :py:class:`LatexEnvironmentNode`. + + .. deprecated:: 1.0 + Please use :py:meth:`LatexWalker.get_latex_environment()` instead. + """ + + return LatexWalker(s, **parse_flags).get_latex_environment( + pos=pos, environmentname=environmentname + ) + + +def get_latex_nodes( + s, + pos=0, + stop_upon_closing_brace=None, + stop_upon_end_environment=None, + stop_upon_closing_mathmode=None, + **parse_flags +): + """ + Parses latex content `s`. + + Returns a tuple `(nodelist, pos, len)` where nodelist is a list of `LatexNode` 's. + + If `stop_upon_closing_brace` is given, then `len` includes the closing brace, but the + closing brace is not included in any of the nodes in the `nodelist`. + + .. deprecated:: 1.0 + Please use :py:meth:`LatexWalker.get_latex_nodes()` instead. + """ + + return LatexWalker(s, **parse_flags).get_latex_nodes( + stop_upon_closing_brace=stop_upon_closing_brace, + stop_upon_end_environment=stop_upon_end_environment, + stop_upon_closing_mathmode=stop_upon_closing_mathmode, + ) + + +# ------------------------------------------------------------------------------ + +# +# small utilities for displaying & debugging +# + + +def nodelist_to_latex(nodelist): + + # It's NOT recommended to use this function. You should use + # node.latex_verbatim() instead. + + # Here, we don't use latex_verbatim() and continue to provide (an updated + # version of) the old code, because we want to be compatible with code that + # used this function on custom instantiated nodes without setting the + # parsing_state. + + def add_args(nodeargd): + if nodeargd is None or nodeargd.argspec is None or nodeargd.argnlist is None: + return "" + argslatex = "" + for argt, argn in zip(nodeargd.argspec, nodeargd.argnlist): + if argt == "*": + if argn is not None: + argslatex += nodelist_to_latex([argn]) + elif argt == "[": + if argn is not None: + # the node is a group node with '[' delimiter char anyway + argslatex += nodelist_to_latex([argn]) + elif argt == "{": + # either a group node with '{' delimiter char, or single node argument + argslatex += nodelist_to_latex([argn]) + else: + raise ValueError("Unknown argument type: {!r}".format(argt)) + return argslatex + + latex = "" + for n in nodelist: + if n is None: + continue + if n.isNodeType(LatexCharsNode): + latex += n.chars + continue + + if n.isNodeType(LatexMacroNode): + latex += r"\%s%s%s" % ( + n.macroname, + n.macro_post_space, + add_args(n.nodeargd), + ) + continue + + if n.isNodeType(LatexSpecialsNode): + latex += r"%s%s" % (n.specials_chars, add_args(n.nodeargd)) + continue + + if n.isNodeType(LatexCommentNode): + latex += "%" + n.comment + n.comment_post_space + continue + + if n.isNodeType(LatexGroupNode): + latex += n.delimiters[0] + nodelist_to_latex(n.nodelist) + n.delimiters[1] + continue + + if n.isNodeType(LatexEnvironmentNode): + latex += r"\begin{%s}%s" % (n.envname, add_args(n.nodeargd)) + latex += nodelist_to_latex(n.nodelist) + latex += r"\end{%s}" % (n.envname) + continue + + if n.isNodeType(LatexMathNode): + latex += n.delimiters[0] + nodelist_to_latex(n.nodelist) + n.delimiters[1] + continue + + latex += "<[UNKNOWN LATEX NODE: '%s']>" % (n.nodeType().__name__) + + return latex + + +def put_in_braces(brace_char, thestring): + # DON'T USE. WILL BE REMOVED IN FUTURE VERSION. + if brace_char == "{": + return "{%s}" % (thestring) + if brace_char == "[": + return "[%s]" % (thestring) + if brace_char == "(": + return "(%s)" % (thestring) + if brace_char == "<": + return "<%s>" % (thestring) + + return brace_char + thestring + brace_char + + +def disp_node(n, indent=0, context="* ", skip_group=False): + # Don't rely upon this function. + title = "" + comment = "" + iterchildren = [] + + def add_args(): + if n.nodeargd is None: + # iterchildren.append(('', '', None)) + return + elif n.nodeargd.argspec is None or n.nodeargd.argnlist is None: + iterchildren.append((" ", "", None)) + return + for argt, argn in zip(n.nodeargd.argspec, n.nodeargd.argnlist): + if argt == "[": + t = "[.]: " + elif argt == "{": + t = "{.}: " + elif argt == "*": + t = "<*>: " + else: + t = ": " + iterchildren.append((t, [argn], False)) + + if n is None: + title = "" + elif n.isNodeType(LatexCharsNode): + title = repr(n.chars) + elif n.isNodeType(LatexMacroNode): + title = "\\" + n.macroname + add_args() + elif n.isNodeType(LatexSpecialsNode): + title = n.specials_chars + " (specials)" + add_args() + elif n.isNodeType(LatexCommentNode): + title = "%" + n.comment.strip() + elif n.isNodeType(LatexGroupNode): + if skip_group: + for nn in n.arg: + disp_node(nn, indent=indent, context=context) + return + title = "Group: " + iterchildren.append(("* ", n.nodelist, False)) + elif n.isNodeType(LatexEnvironmentNode): + title = "\\begin{%s}" % (n.environmentname) + add_args() + iterchildren.append(("* ", n.nodelist, False)) + elif n.isNodeType(LatexMathNode): + title = n.delimiters[0] + n.displaytype + " math" + n.delimiters[1] + iterchildren.append(("* ", n.nodelist, False)) + else: + print("UNKNOWN NODE TYPE: %s" % (n.nodeType().__name__)) + + print(" " * indent + context + title + " " + comment) + + for context, nodelist, skip in iterchildren: + if isinstance(nodelist, _basestring): + print(" " * (indent + 4) + context + nodelist) + continue + for nn in nodelist: + disp_node(nn, indent=indent + 4, context=context, skip_group=skip) + + +def make_json_encoder(latexwalker, use_line_numbers=True): + class LatexNodesJSONEncoder(json.JSONEncoder): + # not official API for now + """ + A :py:class:`json.JSONEncoder` that can encode :py:class:`LatexNode` objects + (and subclasses). + """ + + def __init__(self, *args, **kwargs): + super(LatexNodesJSONEncoder, self).__init__(*args, **kwargs) + + def default(self, obj): + if isinstance(obj, LatexNode): + # Prepare a dictionary with the correct keys and values. + n = obj + d = { + "nodetype": n.__class__.__name__, + } + # redundant_fields = getattr(n, '_redundant_fields', n._fields) + for fld in n._fields: + d[fld] = n.__dict__[fld] + d.update(latexwalker.pos_to_lineno_colno(n.pos, as_dict=True)) + return d + + if isinstance(obj, macrospec.ParsedMacroArgs): + return obj.to_json_object() + + # else: + return super(LatexNodesJSONEncoder, self).default(obj) + + return LatexNodesJSONEncoder diff --git a/good_ai/src/sus/external/pylatexenc/latexwalker/__main__.py b/good_ai/src/sus/external/pylatexenc/latexwalker/__main__.py new file mode 100644 index 0000000..fe95f0b --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexwalker/__main__.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2018 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import argparse +import fileinput +import json +import logging +import sys + +from ..latexwalker import LatexWalker, disp_node, make_json_encoder +from ..version import version_str + + +def main(argv=None): + + if argv is None: + argv = sys.argv[1:] + + parser = argparse.ArgumentParser(prog="latexwalker", add_help=False) + + parser.add_argument( + "--output-format", + metavar="FORMAT", + dest="output_format", + choices=["human", "json"], + default="human", + help='Requested output format for the node tree ("human" or "json")', + ) + parser.add_argument( + "--json-indent", + metavar="NUMSPACES", + dest="json_indent", + type=int, + default=2, + help="Indentation in JSON output (specify number of spaces " + "per indentation level)", + ) + parser.add_argument( + "--json-compact", + dest="json_indent", + action="store_const", + const=None, + help="Output compact JSON", + ) + + parser.add_argument( + "--keep-inline-math", + action="store_const", + const=True, + dest="keep_inline_math", + default=True, + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--no-keep-inline-math", + action="store_const", + const=False, + dest="keep_inline_math", + help=argparse.SUPPRESS, + ) + + parser.add_argument( + "--tolerant-parsing", + action="store_const", + const=True, + dest="tolerant_parsing", + default=True, + ) + parser.add_argument( + "--no-tolerant-parsing", + action="store_const", + const=False, + dest="tolerant_parsing", + help="Tolerate syntax errors when parsing, and attempt " + "to continue (default yes)", + ) + + # I'm not sure this flag is useful and if it should be exposed at all. + # Accept it, but make it hidden. + parser.add_argument( + "--strict-braces", + action="store_const", + const=True, + dest="strict_braces", + default=False, + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--no-strict-braces", + action="store_const", + const=False, + dest="strict_braces", + # help="Report errors for mismatching LaTeX braces (default no)" + help=argparse.SUPPRESS, + ) + + parser.add_argument( + "-q", + "--quiet", + dest="logging_level", + action="store_const", + const=logging.ERROR, + default=logging.INFO, + help="Suppress warning messages", + ) + parser.add_argument( + "-v", + "--verbose", + dest="logging_level", + action="store_const", + const=logging.DEBUG, + help="Verbose output", + ) + parser.add_argument( + "--version", + action="version", + version="pylatexenc {}".format(version_str), + help="Show version information and exit", + ) + parser.add_argument( + "--help", action="help", help="Show this help information and exit" + ) + + parser.add_argument( + "--code", + "-c", + action="store", + default=None, + metavar="LATEX_CODE", + help="Convert the given LATEX_CODE to unicode text instead of reading " + "from FILE or standard input. You cannot specify FILEs if you use this " + "option, and any standard input is ignored.", + ) + + parser.add_argument( + "files", + metavar="FILE", + nargs="*", + help="Input files (if none specified, read from stdandard input)", + ) + + args = parser.parse_args(argv) + + logging.basicConfig() + logging.getLogger().setLevel(args.logging_level) + logger = logging.getLogger(__name__) + + latex = "" + if args.code: + if args.files: + logger.error( + "Cannot specify both FILEs and --code option. " + "Use --help option for more information." + ) + sys.exit(1) + latex = args.code + else: + for line in fileinput.input(files=args.files): + latex += line + + latexwalker = LatexWalker( + latex, tolerant_parsing=args.tolerant_parsing, strict_braces=args.strict_braces + ) + + (nodelist, pos, len_) = latexwalker.get_latex_nodes() + + if args.output_format == "human": + print("\n--- NODES ---\n") + for n in nodelist: + disp_node(n) + print("\n-------------\n") + return + + if args.output_format == "json": + json.dump( + { + "nodelist": nodelist, + }, + sys.stdout, + cls=make_json_encoder(latexwalker), + indent=args.json_indent, + ) + sys.stdout.write("\n") + return + + raise ValueError("Invalid output format: " + args.output_format) + + +def run_main(): + + try: + + main() + + except SystemExit: + raise + except: # lgtm [py/catch-base-exception] + import pdb + import traceback + + traceback.print_exc() + pdb.post_mortem() + + +if __name__ == "__main__": + + # run_main() # debug + main() diff --git a/good_ai/src/sus/external/pylatexenc/latexwalker/_defaultspecs.py b/good_ai/src/sus/external/pylatexenc/latexwalker/_defaultspecs.py new file mode 100644 index 0000000..817ae71 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/latexwalker/_defaultspecs.py @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2019 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +# Internal module. May change without notice. + + +from ..macrospec import ( + EnvironmentSpec, + MacroSpec, + MacroStandardArgsParser, + VerbatimArgsParser, + std_environment, + std_macro, + std_specials, +) + +specs = [ + # + # CATEGORY: latex-base + # + ( + "latex-base", + { + "macros": [ + std_macro("documentclass", True, 1), + std_macro("usepackage", True, 1), + std_macro("RequirePackage", True, 1), + std_macro("selectlanguage", True, 1), + std_macro("setlength", True, 2), + std_macro("addlength", True, 2), + std_macro("setcounter", True, 2), + std_macro("addcounter", True, 2), + std_macro("newcommand", "*{[[{"), + std_macro("renewcommand", "*{[[{"), + std_macro("providecommand", "*{[[{"), + std_macro("newenvironment", "*{[[{{"), + std_macro("renewenvironment", "*{[[{{"), + std_macro("provideenvironment", "*{[[{{"), + std_macro("DeclareMathOperator", "*{{"), + std_macro("hspace", "*{"), + std_macro("vspace", "*{"), + MacroSpec( + "mbox", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + # \title, \author, \date + MacroSpec("title", "{"), + MacroSpec("author", "{"), + MacroSpec("date", "{"), + # (Note: single backslash) end of line with optional no-break ('*') and + # additional vertical spacing, e.g. \\*[2mm] + # + # Special for this command: don't allow an optional spacing argument + # [2mm] to be separated by spaces from the rest of the macro. This + # emulates the behavior in AMS environments, and avoids some errors; + # e.g. in "\begin{align} A=0 \\ [C,D]=0 \end{align}" the "[C,D]" + # does not get captured as an optional macro argument. + MacroSpec( + "\\", + args_parser=MacroStandardArgsParser( + "*[", optional_arg_no_space=True + ), + ), + std_macro("item", True, 0), + # \input{someotherfile} + std_macro("input", False, 1), + std_macro("include", False, 1), + std_macro("includegraphics", True, 1), + std_macro("chapter", "*[{"), + std_macro("section", "*[{"), + std_macro("subsection", "*[{"), + std_macro("subsubsection", "*[{"), + std_macro("pagagraph", "*[{"), + std_macro("subparagraph", "*[{"), + std_macro("bibliography", "{"), + std_macro("emph", False, 1), + MacroSpec( + "textrm", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "textit", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "textbf", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "textmd", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "textsc", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "textsf", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "textsl", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "texttt", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "textup", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + MacroSpec( + "text", + args_parser=MacroStandardArgsParser("{", args_math_mode=[False]), + ), + std_macro("mathrm", False, 1), # only allowed in math mode anyway + std_macro("mathbb", False, 1), # only allowed in math mode anyway + std_macro("mathbf", False, 1), + std_macro("mathit", False, 1), + std_macro("mathsf", False, 1), + std_macro("mathtt", False, 1), + std_macro("mathcal", False, 1), + std_macro("mathscr", False, 1), + std_macro("mathfrak", False, 1), + std_macro("label", False, 1), + std_macro("ref", False, 1), + std_macro("autoref", False, 1), + std_macro("cref", False, 1), + std_macro("Cref", False, 1), + std_macro("eqref", False, 1), + std_macro("url", False, 1), + std_macro("hypersetup", False, 1), + std_macro("footnote", True, 1), + std_macro("keywords", False, 1), + std_macro("hphantom", True, 1), + std_macro("vphantom", True, 1), + std_macro("'", False, 1), + std_macro("`", False, 1), + std_macro('"', False, 1), + std_macro("c", False, 1), + std_macro("^", False, 1), + std_macro("~", False, 1), + std_macro("H", False, 1), + std_macro("k", False, 1), + std_macro("=", False, 1), + std_macro("b", False, 1), + std_macro(".", False, 1), + std_macro("d", False, 1), + std_macro("r", False, 1), + std_macro("u", False, 1), + std_macro("v", False, 1), + MacroSpec( + "ensuremath", + args_parser=MacroStandardArgsParser("{", args_math_mode=[True]), + ), + std_macro("not", False, 1), + std_macro("vec", False, 1), + std_macro("dot", False, 1), + std_macro("hat", False, 1), + std_macro("check", False, 1), + std_macro("breve", False, 1), + std_macro("acute", False, 1), + std_macro("grave", False, 1), + std_macro("tilde", False, 1), + std_macro("bar", False, 1), + std_macro("ddot", False, 1), + std_macro("frac", False, 2), + std_macro("nicefrac", False, 2), + std_macro("sqrt", True, 1), + MacroSpec("overline", "{"), + MacroSpec("underline", "{"), + MacroSpec("widehat", "{"), + MacroSpec("widetilde", "{"), + MacroSpec("wideparen", "{"), + MacroSpec("overleftarrow", "{"), + MacroSpec("overrightarrow", "{"), + MacroSpec("overleftrightarrow", "{"), + MacroSpec("underleftarrow", "{"), + MacroSpec("underrightarrow", "{"), + MacroSpec("underleftrightarrow", "{"), + MacroSpec("overbrace", "{"), + MacroSpec("underbrace", "{"), + MacroSpec("overgroup", "{"), + MacroSpec("undergroup", "{"), + MacroSpec("overbracket", "{"), + MacroSpec("underbracket", "{"), + MacroSpec("overlinesegment", "{"), + MacroSpec("underlinesegment", "{"), + MacroSpec("overleftharpoon", "{"), + MacroSpec("overrightharpoon", "{"), + MacroSpec("xleftarrow", "[{"), + MacroSpec("xrightarrow", "[{"), + std_macro("ket", False, 1), + std_macro("bra", False, 1), + std_macro("braket", False, 2), + std_macro("ketbra", False, 2), + std_macro("texorpdfstring", False, 2), + # xcolor commands + MacroSpec("definecolor", "[{{{"), + MacroSpec("providecolor", "[{{{"), + MacroSpec("colorlet", "[{[{"), + MacroSpec("color", "[{"), + MacroSpec("textcolor", "[{{"), + MacroSpec("pagecolor", "[{"), + MacroSpec("nopagecolor", ""), + MacroSpec("colorbox", "[{{"), + MacroSpec("fcolorbox", "[{[{{"), + MacroSpec("boxframe", "{{{"), + MacroSpec("rowcolors", "*[{{{"), + ], + "environments": [ + # NOTE: Starred variants (as in \begin{equation*}) are not specified as + # for macros with an argspec='*'. Rather, we need to define a separate + # spec for the starred variant as the star really is part of the + # environment name. If you specify argspec='*', the parser will try to + # look for an expression of the form '\begin{equation}*' + std_environment("figure", "["), + std_environment("figure*", "["), + std_environment("table", "["), + std_environment("table*", "["), + std_environment("abstract", None), + std_environment("tabular", "{"), + std_environment("tabular*", "{{"), + std_environment("tabularx", "{[{"), + std_environment("array", "[{"), + std_environment("equation", None, is_math_mode=True), + std_environment("equation*", None, is_math_mode=True), + std_environment("eqnarray", None, is_math_mode=True), + std_environment("eqnarray*", None, is_math_mode=True), + # AMS environments + std_environment("align", None, is_math_mode=True), + std_environment("align*", None, is_math_mode=True), + std_environment("gather", None, is_math_mode=True), + std_environment("gather*", None, is_math_mode=True), + std_environment("flalign", None, is_math_mode=True), + std_environment("flalign*", None, is_math_mode=True), + std_environment("multline", None, is_math_mode=True), + std_environment("multline*", None, is_math_mode=True), + std_environment("alignat", "{", is_math_mode=True), + std_environment("alignat*", "{", is_math_mode=True), + std_environment("split", None, is_math_mode=True), + ], + "specials": [ + std_specials("&"), + # TODO --- for this, we need to parse their argument but don't use + # the standard args parser because we need to be able to + # accept arguments like "x_\mathrm{initial}" + # + # std_specials('^'), + # std_specials('_'), + ], + }, + ), + # + # CATEGORY: nonascii-specials + # + ( + "nonascii-specials", + { + "macros": [], + "environments": [], + "specials": [ + std_specials("~"), + # cf. https://tex.stackexchange.com/a/439652/32188 "fake ligatures": + std_specials("``"), + std_specials("''"), + std_specials("--"), + std_specials("---"), + std_specials("!`"), + std_specials("?`"), + ], + }, + ), + # + # CATEGORY: verbatim + # + ( + "verbatim", + { + "macros": [ + MacroSpec( + "verb", + args_parser=VerbatimArgsParser(verbatim_arg_type="verb-macro"), + ), + ], + "environments": [ + EnvironmentSpec( + "verbatim", + args_parser=VerbatimArgsParser( + verbatim_arg_type="verbatim-environment" + ), + ), + ], + "specials": [ + # optionally users could include the specials "|" like in latex-doc + # for verbatim |\like \this|... + ], + }, + ), + # + # CATEGORY: theorems + # + ( + "theorems", + { + "macros": [], + "environments": [ + std_environment("theorem", "["), + std_environment("proposition", "["), + std_environment("lemma", "["), + std_environment("corollary", "["), + std_environment("definition", "["), + std_environment("conjecture", "["), + std_environment("remark", "["), + # + std_environment("proof", "["), + # short names + std_environment("thm", "["), + std_environment("prop", "["), + std_environment("lem", "["), + std_environment("cor", "["), + std_environment("conj", "["), + std_environment("rem", "["), + std_environment("defn", "["), + ], + "specials": [], + }, + ), + # + # CATEGORY: enumitem + # + ( + "enumitem", + { + "macros": [], + "environments": [ + std_environment("enumerate", "["), + std_environment("itemize", "["), + std_environment("description", "["), + ], + "specials": [], + }, + ), + # + # CATEGORY: natbib + # + ( + "natbib", + { + "macros": [ + std_macro("cite", "*[[{"), + std_macro("citet", "*[[{"), + std_macro("citep", "*[[{"), + std_macro("citealt", "*[[{"), + std_macro("citealp", "*[[{"), + std_macro("citeauthor", "*[[{"), + std_macro("citefullauthor", "[[{"), + std_macro("citeyear", "[[{"), + std_macro("citeyearpar", "[[{"), + std_macro("Citet", "*[[{"), + std_macro("Citep", "*[[{"), + std_macro("Citealt", "*[[{"), + std_macro("Citealp", "*[[{"), + std_macro("Citeauthor", "*[[{"), + std_macro("citetext", "{"), + std_macro("citenum", "{"), + std_macro("defcitealias", "{{"), + std_macro("citetalias", "[[{"), + std_macro("citepalias", "[[{"), + ], + "environments": [], + "specials": [], + }, + ), + # + # CATEGORY: latex-ethuebung + # + ( + "latex-ethuebung", + { + "macros": [ + # ethuebung + std_macro("UebungLoesungFont", False, 1), + std_macro("UebungHinweisFont", False, 1), + std_macro("UebungExTitleFont", False, 1), + std_macro("UebungSubExTitleFont", False, 1), + std_macro("UebungTipsFont", False, 1), + std_macro("UebungLabel", False, 1), + std_macro("UebungSubLabel", False, 1), + std_macro("UebungLabelEnum", False, 1), + std_macro("UebungLabelEnumSub", False, 1), + std_macro("UebungSolLabel", False, 1), + std_macro("UebungHinweisLabel", False, 1), + std_macro("UebungHinweiseLabel", False, 1), + std_macro("UebungSolEquationLabel", False, 1), + std_macro("UebungTipsLabel", False, 1), + std_macro("UebungTipsEquationLabel", False, 1), + std_macro("UebungsblattTitleSeries", False, 1), + std_macro("UebungsblattTitleSolutions", False, 1), + std_macro("UebungsblattTitleTips", False, 1), + std_macro("UebungsblattNumber", False, 1), + std_macro("UebungsblattTitleFont", False, 1), + std_macro("UebungTitleCenterVSpacing", False, 1), + std_macro("UebungAttachedSolutionTitleTop", False, 1), + std_macro("UebungAttachedSolutionTitleFont", False, 1), + std_macro("UebungAttachedSolutionTitle", False, 1), + std_macro("UebungTextAttachedSolution", False, 1), + std_macro("UebungDueByLabel", False, 1), + std_macro("UebungDueBy", False, 1), + std_macro("UebungLecture", False, 1), + std_macro("UebungProf", False, 1), + std_macro("UebungLecturer", False, 1), + std_macro("UebungSemester", False, 1), + std_macro("UebungLogoFile", False, 1), + std_macro("UebungLanguage", False, 1), + std_macro("UebungStyle", False, 1), + # + std_macro("uebung", "{["), + std_macro("exercise", "{["), + std_macro("keywords", False, 1), + std_macro("subuebung", False, 1), + std_macro("subexercise", False, 1), + std_macro("pdfloesung", True, 1), + std_macro("pdfsolution", True, 1), + std_macro("exenumfulllabel", False, 1), + std_macro("hint", False, 1), + std_macro("hints", False, 1), + std_macro("hinweis", False, 1), + std_macro("hinweise", False, 1), + ], + "environments": [], + "specials": [], + }, + ), +] diff --git a/good_ai/src/sus/external/pylatexenc/macrospec/__init__.py b/good_ai/src/sus/external/pylatexenc/macrospec/__init__.py new file mode 100644 index 0000000..9975837 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/macrospec/__init__.py @@ -0,0 +1,785 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2019 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +r""" +Provides classes and helper functions to describe a LaTeX context of known +macros and environments, specifying how they should be parsed by +:py:mod:`pylatexenc.latexwalker`. + +.. versionadded:: 2.0 + + The entire module :py:mod:`pylatexenc.macrospec` was introduced in + `pylatexenc 2.0`. +""" + + +import sys + +if sys.version_info.major > 2: + # Py3 + def unicode(s): + return s + + _basestring = str + _str_from_unicode = lambda x: x + _unicode_from_str = lambda x: x +else: + # Py2 + _basestring = basestring + _str_from_unicode = lambda x: unicode(x).encode("utf-8") + _unicode_from_str = lambda x: x.decode("utf-8") + + +# ------------------------------------------------------------------------------ + +from ._argparsers import ( + MacroStandardArgsParser, + ParsedMacroArgs, + ParsedVerbatimArgs, + VerbatimArgsParser, +) + +# ------------------------------------------------------------------------------ + + +class MacroSpec(object): + r""" + Stores the specification of a macro. + + This stores the macro name and instructions on how to parse the macro + arguments. + + .. py:attribute:: macroname + + The name of the macro, without the leading backslash. + + .. py:attribute:: args_parser + + The parser instance that can understand this macro's arguments. For + standard LaTeX macros this is usually a + :py:class:`MacroStandardArgsParser` instance. + + If you specify a string, then for convenience this is interpreted as an + argspec argument for :py:class:`MacroStandardArgsParser` and such an + instance is automatically created. + """ + + def __init__(self, macroname, args_parser=MacroStandardArgsParser(), **kwargs): + super(MacroSpec, self).__init__(**kwargs) + self.macroname = macroname + if isinstance(args_parser, _basestring): + self.args_parser = MacroStandardArgsParser(args_parser) + else: + self.args_parser = args_parser + + def parse_args(self, *args, **kwargs): + r""" + Shorthand for calling the :py:attr:`args_parser`\ 's `parse_args()` method. + See :py:class:`MacroStandardArgsParser`. + """ + return self.args_parser.parse_args(*args, **kwargs) + + def __repr__(self): + return "MacroSpec(macroname=%r, args_parser=%r)" % ( + self.macroname, + self.args_parser, + ) + + +class EnvironmentSpec(object): + r""" + Stores the specification of a LaTeX environment. + + This stores the environment name and instructions on how to parse any + arguments provided after ``\begin{environment}``. + + .. py:attribute:: environmentname + + The name of the environment, i.e., the argument of ``\begin{...}`` and + ``\end{...}``. + + .. py:attribute:: args_parser + + The parser instance that can understand this environment's arguments. + For standard LaTeX environment this is usually a + :py:class:`MacroStandardArgsParser` instance. + + If you specify a string, then for convenience this is interpreted as an + argspec argument for :py:class:`MacroStandardArgsParser` and such an + instance is automatically created. + + .. py:attribute:: is_math_mode + + A boolean that indicates whether or not the contents is to be interpreted + in Math Mode. This would be True for environments like + ``\begin{equation}``, ``\begin{align}``, etc., but False for + ``\begin{figure}``, etc. + + .. note:: + + Starred variants of environments (as in ``\begin{equation*}``) must not + be specified using an argspec as for macros (e.g., `argspec='*'`). + Rather, we need to define a separate environment spec for the starred + variant with the star in the name itself (``EnvironmentSpec('equation*', + None)``) because the star really is part of the environment name. If you + happened to use ``EnvironmentSpec('equation', '*')``, then the parser + would recognize the expression ``\begin{equation}*`` but not + ``\begin{equation*}``. + """ + + def __init__( + self, + environmentname, + args_parser=MacroStandardArgsParser(), + is_math_mode=False, + **kwargs + ): + super(EnvironmentSpec, self).__init__(**kwargs) + self.environmentname = environmentname + if isinstance(args_parser, _basestring): + self.args_parser = MacroStandardArgsParser(args_parser) + else: + self.args_parser = args_parser + self.is_math_mode = is_math_mode + + def parse_args(self, *args, **kwargs): + r""" + Shorthand for calling the :py:attr:`args_parser`\ 's `parse_args()` method. + See :py:class:`MacroStandardArgsParser`. + """ + return self.args_parser.parse_args(*args, **kwargs) + + def __repr__(self): + return ( + "EnvironmentSpec(environmentname=%r, args_parser=%r, is_math_mode=%r)" + % (self.environmentname, self.args_parser, self.is_math_mode) + ) + + +class SpecialsSpec(object): + r""" + Specification of a LaTeX "special char sequence": an active char, a + ligature, or some other non-macro char sequence that has a special meaning. + + For instance, '&', '~', and '``' are considered as "specials". + + .. py:attribute:: specials_chars + + The string (one or several characters) that has a special meaning. E.g., + '&', '~', '``', etc. + + .. py:attribute:: args_parser + + A parser (e.g. :py:class:`MacroStandardArgsParser`) that is invoked when + the specials is encountered. Can/should be set to `None` if the specials + should not parse any arguments (e.g. '~'). + """ + + def __init__(self, specials_chars, args_parser=None, **kwargs): + super(SpecialsSpec, self).__init__(**kwargs) + self.specials_chars = specials_chars + self.args_parser = args_parser + + def parse_args(self, *args, **kwargs): + r""" + Basically a shorthand for calling the :py:attr:`args_parser`\ 's + `parse_args()` method. See :py:class:`MacroStandardArgsParser`. + + If however the py:attr:`args_parser` attribute is `None`, then this + method returns `None`. + """ + if self.args_parser is None: + return None + return self.args_parser.parse_args(*args, **kwargs) + + def __repr__(self): + return "SpecialsSpec(specials_chars=%r, args_parser=%r)" % ( + self.specials_chars, + self.args_parser, + ) + + +# ------------------------------------------------------------------------------ + + +def std_macro(macname, *args, **kwargs): + r""" + Return a macro specification for the given macro. Syntax:: + + spec = std_macro(macname, argspec) + # or + spec = std_macro(macname, optarg, numargs) + # or + spec = std_macro( (macname, argspec), ) + # or + spec = std_macro( (macname, optarg, numargs), ) + # or + spec = std_macro( spec ) # spec is already a `MacroSpec` -- no-op + + - `macname` is the name of the macro, without the leading backslash. + + - `argspec` is a string either characters "\*", "{" or "[", in which star + indicates an optional asterisk character (e.g. starred macro variants), + each curly brace specifies a mandatory argument and each square bracket + specifies an optional argument in square brackets. For example, "{{\*[{" + expects two mandatory arguments, then an optional star, an optional + argument in square brackets, and then another mandatory argument. + + `argspec` may also be `None`, which is the same as ``argspec=''``. + + - `optarg` may be one of `True`, `False`, or `None`, corresponding to these + possibilities: + + + if `True`, the macro expects as first argument an optional argument in + square brackets. Then, `numargs` specifies the number of additional + mandatory arguments to the command, given in usual curly braces (or + simply as one TeX token like a single macro) + + + if `False`, the macro only expects a number of mandatory arguments given + by `numargs`. The mandatory arguments are given in usual curly braces + (or simply as one TeX token like a single macro) + + + if `None`, then `numargs` is a string like `argspec` above. I.e., + ``std_macro(macname, None, argspec)`` is the same as + ``std_macro(macname, argspec)``. + + - `numargs`: depends on `optarg`, see above. + + To make environment specifications (:py:class:`EnvironmentSpec`) instead of + a macro specification, use the function :py:func:`std_environment()` + instead. + + The helper function :py:func:`std_environment()` is a shorthand for calling + this function with additional keyword arguments. An optional keyword + argument `make_environment_spec=True` to the present function may be + specified to return an `EnvironmentSpec` instead of a `MacroSpec`. In this + case, you can further specify the `environment_is_math_mode=True|False` to + specify whether of not the environment represents a math mode. + """ + + if isinstance(macname, tuple): + if len(args) != 0: + raise TypeError( + "No positional arguments expected if first argument is a tuple" + ) + args = tuple(macname[1:]) + macname = macname[0] + + if isinstance(macname, MacroSpec): + if len(args) != 0: + raise TypeError( + "No positional arguments expected if first argument is a MacroSpec" + ) + return macname + + if isinstance(macname, EnvironmentSpec): + if len(args) != 0: + raise TypeError( + "No positional arguments expected if first argument is a EnvironmentSpec" + ) + return macname + + if len(args) == 1: + # std_macro(macname, argspec) + argspec = args[0] + elif len(args) != 2: + raise TypeError( + "Wrong number of arguments for std_macro, macname={!r}, args={!r}".format( + macname, args + ) + ) + elif not args[0] and isinstance(args[1], _basestring): + # argspec given in numargs + argspec = args[1] + else: + argspec = "" + if args[0]: + argspec = "[" + argspec += "{" * args[1] + + if kwargs.get("make_environment_spec", False): + return EnvironmentSpec( + macname, + args_parser=MacroStandardArgsParser(argspec), + is_math_mode=kwargs.get("environment_is_math_mode", False), + ) + return MacroSpec(macname, args_parser=MacroStandardArgsParser(argspec)) + + +def std_environment(envname, *args, **kwargs): + r""" + Return an environment specification for the given environment. Syntax:: + + spec = std_environment(envname, argspec, is_math_mode=True|False) + # or + spec = std_environment(envname, optarg, numargs, is_math_mode=True|False) + # or + spec = std_environment( (envname, argspec), is_math_mode=True|False) + # or + spec = std_environment( (envname, optarg, numargs), is_math_mode=True|False) + # or + spec = std_environment( spec ) # spec is already a `EnvironmentSpec` -- no-op + + - `envname` is the name of the environment, i.e., the argument to + ``\begin{...}``. + + - `argspec` is a string either characters "\*", "{" or "[", in which star + indicates an optional asterisk character (e.g. starred environment + variants), each curly brace specifies a mandatory argument and each square + bracket specifies an optional argument in square brackets. For example, + "{{\*[{" expects two mandatory arguments, then an optional star, an + optional argument in square brackets, and then another mandatory argument. + + `argspec` may also be `None`, which is the same as ``argspec=''``. + + .. note:: + + See :py:class:`EnvironmentSpec` for an important remark about starred + variants for environments. TL;DR: a starred verison of an environment is + defined as a separate `EnvironmentSpec` with the star in the name and + *not* using an ``argspec='*'``. + + - `optarg` may be one of `True`, `False`, or `None`, corresponding to these + possibilities: + + + if `True`, the environment expects as first argument an optional argument in + square brackets. Then, `numargs` specifies the number of additional + mandatory arguments to the command, given in usual curly braces (or + simply as one TeX token like a single environment) + + + if `False`, the environment only expects a number of mandatory arguments given + by `numargs`. The mandatory arguments are given in usual curly braces + (or simply as one TeX token like a single environment) + + + if `None`, then `numargs` is a string like `argspec` above. I.e., + ``std_environment(envname, None, argspec)`` is the same as + ``std_environment(envname, argspec)``. + + - `numargs`: depends on `optarg`, see above. + + - `is_math_mode`: if set to True, then the environment represents a math + mode environment (e.g., 'equation', 'align', 'gather', etc.), i.e., whose + contents should be parsed in an appropriate math mode. Note that + `is_math_mode` *must* be given as a keyword argument, in contrast to all + other arguments which must be positional (non-keyword) arguments. + """ + is_math_mode = kwargs.pop("is_math_mode", False) + kwargs2 = dict(kwargs) + kwargs2.update(make_environment_spec=True, environment_is_math_mode=is_math_mode) + return std_macro(envname, *args, **kwargs2) + + +def std_specials(specials_chars): + r""" + Return a latex specials specification for the given character sequence. Syntax:: + + spec = std_specials(specials_chars) + + where `specials_chars` is the sequence of characters that has a special + LaTeX meaning, e.g. ``&`` or ``''``. + + This helper function only allows to create specs for simple specials without + any argument parsing. For more complicated specials, you can instantiate a + :py:class:`SpecialsSpec` directly. + """ + return SpecialsSpec(specials_chars, args_parser=None) + + +# ------------------------------------------------------------------------------ + + +class LatexContextDb(object): + r""" + Store a database of specifications of known macros, environments, and other + latex specials. This might be, e.g., how many arguments a macro accepts, or + how to determine the text representation of a macro or environment. + + When used with :py:class:`pylatexenc.latexwalker.LatexWalker`, the + specifications describe mostly rules for parsing arguments of macros and + environments, and which sequences of characters to consider as "latex + specials". Specifications for macros, environments, and other specials are + stored as :py:class:`MacroSpec`, :py:class:`EnvironmentSpec`, and + :py:class:`SpecialsSpec` instances, respectively. + When used with :py:class:`pylatexenc.latex2text.LatexNodes2Text`, the + specifications for macros, environments, and other specials are stored as + :py:class:`pylatexenc.latex2text.MacroTextSpec` , + :py:class:`pylatexenc.latex2text.EnvironmentTextSpec`, and + :py:class:`pylatexenc.latex2text.SpecialsTextSpec` instances, respectively. + + In fact, the objects stored in this database may be of any type, except that + macro specifications must have an attribute `macroname`, environment + specifications must have an attribute `environmentname`, and specials + specification must have an attribute `specials_chars`. + + The `LatexContextDb` instance is meant to be (pseudo-)immutable. Once + constructed and all the definitions added with + :py:meth:`add_context_category()`, one should refrain from modifying it + directly after providing it to, e.g., a + :py:class:`~pylatexenc.latexwalker.LatexWalker` object. The reason is that + the latex walker keeps track of what the latex context was when parsing + nodes, and modifying the context will modify that stored information, too. + Instead of being tempted to modify the object, create a new one with + :py:meth:`filter_context()`. + + See :py:func:`pylatexenc.latexwalker.get_default_latex_context_db()` for the + default latex context for `latexwalker` with a default collection of known + latex macros and environments. + See :py:func:`pylatexenc.latex2text.get_default_latex_context_db()` for the + default latex context for `latex2text` with a set of text replacements for a + collection of known macros and environments. + """ + + def __init__(self, **kwargs): + super(LatexContextDb, self).__init__(**kwargs) + + self.category_list = [] + self.d = {} + + self.unknown_macro_spec = None + self.unknown_environment_spec = None + self.unknown_specials_spec = None + + def add_context_category( + self, + category, + macros=[], + environments=[], + specials=[], + prepend=False, + insert_before=None, + insert_after=None, + ): + r""" + Register a category of macro and environment specifications in the context + database. + + The category name `category` must not already exist in the database. + + The argument `macros` is an iterable (e.g., a list) of macro + specification objects. The argument `environments` is an iterable + (e.g., a list) of environment spec objects. Similarly, the `specials` + argument is an iterable of latex specials spec instances. + + If you specify `prepend=True`, then macro and environment lookups will + prioritize this category over other categories. Categories are normally + searched for in the order they are registered to the database; if you + specify `prepend=True`, then the new category is prepended to the + existing list so that it is searched first. + + If `insert_before` is not `None`, then it must be a string; the + definitions are inserted in the category list immediately before the + given category name, or at the beginning of the list if the given + category doesn't exist. If `insert_after` is not `None`, then it must + be a string; the definitions are inserted in the category list + immediately after the given category name, or at the end of the list if + the given category doesn't exist. + + You may only specify one of `prepend=True`, `insert_before='...'` or + `insert_after='...'`. + """ + + if category in self.category_list: + raise ValueError( + "Category {} is already registered in the context database".format( + category + ) + ) + + # ensure only one of these options is set + if len([x for x in (prepend, insert_before, insert_after) if x]) > 1: + raise TypeError( + "add_context_category(): You may only specify one of " + "prepend=True, insert_before=... or insert_after=..." + ) + + if prepend: + self.category_list.insert(0, category) + elif insert_before: + if insert_before in self.category_list: + i = self.category_list.index(insert_before) + else: + i = 0 + self.category_list.insert(i, category) + elif insert_after: + if insert_after in self.category_list: + i = ( + self.category_list.index(insert_after) + 1 + ) # insert after found category + else: + i = len(self.category_list) + self.category_list.insert(i, category) + else: + self.category_list.append(category) + + self.d[category] = { + "macros": dict((m.macroname, m) for m in macros), + "environments": dict((e.environmentname, e) for e in environments), + "specials": dict((s.specials_chars, s) for s in specials), + } + + def set_unknown_macro_spec(self, macrospec): + r""" + Set the macro spec to use when encountering a macro that is not in the + database. + """ + self.unknown_macro_spec = macrospec + + def set_unknown_environment_spec(self, environmentspec): + r""" + Set the environment spec to use when encountering a LaTeX environment that + is not in the database. + """ + self.unknown_environment_spec = environmentspec + + def set_unknown_specials_spec(self, specialsspec): + r""" + Set the latex specials spec to use when encountering a LaTeX environment + that is not in the database. + """ + self.unknown_specials_spec = specialsspec + + def categories(self): + r""" + Return a list of valid category names that are registered in the current + database context. + """ + return list(self.category_list) + + def get_macro_spec(self, macroname): + r""" + Look up a macro specification by macro name. The macro name is searched for + in all categories one by one and the first match is returned. + + Returns a macro spec instance that matches the given `macroname`. If + the macro name was not found, we return the default macro specification + set by :py:meth:`set_unknown_macro_spec()` or `None` if no such spec was + set. + """ + for cat in self.category_list: + # search categories in the given order + if macroname in self.d[cat]["macros"]: + return self.d[cat]["macros"][macroname] + return self.unknown_macro_spec + + def get_environment_spec(self, environmentname): + r""" + Look up an environment specification by environment name. The environment + name is searched for in all categories one by one and the first match is + returned. + + Returns the environment spec. If the environment name was not found, we + return the default environment specification set by + :py:meth:`set_unknown_environment_spec()` or `None` if no such spec was + set. + """ + for cat in self.category_list: + # search categories in the given order + if environmentname in self.d[cat]["environments"]: + return self.d[cat]["environments"][environmentname] + return self.unknown_environment_spec + + def get_specials_spec(self, specials_chars): + r""" + Look up a "latex specials" specification by character sequence. The + sequence name is searched for in all categories one by one and the first + match is returned. + + If you are parsing a chunk of LaTeX code, you should use + :py:meth:`test_for_specials()` instead. Unlike + :py:meth:`test_for_specials()`, :py:meth:`get_specials_spec()` returns + the first match regardless of matched length. [Rationale: we only need + to worry about matching the longest specials sequence when parsing LaTeX + code. Calling `get_specials_spec()` means one has already parsed the + sequence and one is looking up additional specs on it.] + + Returns the specials spec. If the latex specials was not found, we + return the default latex specials specification set by + :py:meth:`set_unknown_specials_spec()` or `None` if no such spec was + set. + """ + for cat in self.category_list: + # search categories in the given order + if specials_chars in self.d[cat]["specials"]: + return self.d[cat]["specials"][specials_chars] + return self.unknown_specials_spec + + def test_for_specials(self, s, pos, parsing_state=None): + r""" + Test the given position in the string for any LaTeX specials. The lookup + proceeds by searching for in all categories one by one and the first + match is returned, except that the longest match accross all categories + is returned. For instance, a match of '``' in a later category will + take precedence over a match of '`' in a earlier-searched category. + + Returns a specials spec instance, or `None` if no specials are detected + at the position `pos`. + """ + best_match_len = 0 + best_match_s = None + for cat in self.category_list: + # search categories in the given order + for specials_chars in self.d[cat]["specials"].keys(): + if len(specials_chars) > best_match_len and s.startswith( + specials_chars, pos + ): + best_match_s = self.d[cat]["specials"][specials_chars] + best_match_len = len(specials_chars) + + return best_match_s # this is None if no match + + def iter_macro_specs(self, categories=None): + r""" + Yield the macro specs corresponding to all macros in the given categories. + + If `categories` is `None`, then the known macro specs from all + categories are provided in one long iterable sequence. Otherwise, + `categories` should be a list or iterable of category names (e.g., + 'latex-base') of macro specs to return. + + The macro specs from the different categories specified are concatenated + into one long sequence which is yielded spec by spec. + """ + + if categories is None: + categories = self.category_list + + for c in categories: + if c not in self.category_list: + raise ValueError( + "Invalid latex macro spec db category: {!r} (Expected one of {!r})".format( + c, self.category_list + ) + ) + for spec in self.d[c]["macros"].values(): + yield spec + + def iter_environment_specs(self, categories=None): + r""" + Yield the environment specs corresponding to all environments in the given + categories. + + If `categories` is `None`, then the known environment specs from all + categories are provided in one long iterable sequence. Otherwise, + `categories` should be a list or iterable of category names (e.g., + 'latex-base') of environment specs to return. + + The environment specs from the different categories specified are + concatenated into one long sequence which is yielded spec by spec. + """ + + if categories is None: + categories = self.category_list + + for c in categories: + if c not in self.category_list: + raise ValueError( + "Invalid latex environment spec db category: {!r} (Expected one of {!r})".format( + c, self.category_list + ) + ) + for spec in self.d[c]["environments"].values(): + yield spec + + def iter_specials_specs(self, categories=None): + r""" + Yield the specials specs corresponding to all environments in the given + categories. + + If `categories` is `None`, then the known specials specs from all + categories are provided in one long iterable sequence. Otherwise, + `categories` should be a list or iterable of category names (e.g., + 'latex-base') of specials specs to return. + + The specials specs from the different categories specified are + concatenated into one long sequence which is yielded spec by spec. + """ + + if categories is None: + categories = self.category_list + + for c in categories: + if c not in self.category_list: + raise ValueError( + "Invalid latex environment spec db category: {!r} (Expected one of {!r})".format( + c, self.category_list + ) + ) + for spec in self.d[c]["specials"].values(): + yield spec + + def filter_context(self, keep_categories=[], exclude_categories=[], keep_which=[]): + r""" + Return a new :py:class:`LatexContextDb` instance where we only keep + certain categories of macro and environment specifications. + + If `keep_categories` is set to a nonempty list, then the returned + context will not contain any definitions that do not correspond to the + specified categories. + + If `exclude_categories` is set to a nonempty list, then the returned + context will not contain any definitions that correspond to the + specified categories. + + It is explicitly fine to have category names in `keep_categories` and + `exclude_categories` that don't exist in the present object + (cf. :py:meth:`categories()`). + + The argument `keep_which`, if non-empty, specifies which definitions to + keep. It should be a subset of the list ['macros', 'environments', + 'specials']. + + The returned context will make a copy of the dictionaries that store the + macro and environment specifications, but the specification classes (and + corresponding argument parsers) might correspond to the same instances. + I.e., the returned context is not a full deep copy. + """ + + new_context = LatexContextDb() + + new_context.unknown_macro_spec = self.unknown_macro_spec + new_context.unknown_environment_spec = self.unknown_environment_spec + new_context.unknown_specials_spec = self.unknown_specials_spec + + keep_macros = not keep_which or "macros" in keep_which + keep_environments = not keep_which or "environments" in keep_which + keep_specials = not keep_which or "specials" in keep_which + + for cat in self.category_list: + if keep_categories and cat not in keep_categories: + continue + if exclude_categories and cat in exclude_categories: + continue + + # include this category + new_context.add_context_category( + cat, + macros=self.d[cat]["macros"].values() if keep_macros else [], + environments=self.d[cat]["environments"].values() + if keep_environments + else [], + specials=self.d[cat]["specials"].values() if keep_specials else [], + ) + + return new_context diff --git a/good_ai/src/sus/external/pylatexenc/macrospec/_argparsers.py b/good_ai/src/sus/external/pylatexenc/macrospec/_argparsers.py new file mode 100644 index 0000000..7fb20e8 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/macrospec/_argparsers.py @@ -0,0 +1,497 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2019 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +# Internal module. Internal API may move, disappear or otherwise change at any +# time and without notice. + + +import sys + +if sys.version_info.major > 2: + # Py3 + def unicode(s): + return s + + _basestring = str + _str_from_unicode = lambda x: x + _unicode_from_str = lambda x: x +else: + # Py2 + _basestring = basestring + _str_from_unicode = lambda x: unicode(x).encode("utf-8") + _unicode_from_str = lambda x: x.decode("utf-8") + + +class ParsedMacroArgs(object): + r""" + Parsed representation of macro arguments. + + The base class provides a simple way of storing the arguments as a list of + parsed nodes. + + This base class can be subclassed to store additional information and + provide more advanced APIs to access macro arguments for certain categories + of macros. + + Arguments: + + - `argnlist` is a list of latexwalker nodes that represent macro + arguments. If the macro arguments are too complicated to store in a + list, leave this as `None`. (But then code that uses the latexwalker + must be aware of your own API to access the macro arguments.) + + The difference between `argnlist` and the legacy `nodeargs` is that all + options, regardless of optional or mandatory, are stored in the list + `argnlist` with possible `None`\ 's at places where optional arguments + were not provided. Previously, whether a first optional argument was + included in `nodeoptarg` or `nodeargs` depended on how the macro + specification was given. + + - `argspec` is a string or a list that describes how each corresponding + argument in `argnlist` represents. If the macro arguments are too + complicated to store in a list, leave this as `None`. For standard + macros and parsed arguments this is a string with characters '*', '[', + '{' describing an optional star argument, an optional + square-bracket-delimited argument, and a mandatory argument. + + Attributes: + + .. py:attribute:: argnlist + + The list of latexwalker nodes that was provided to the constructor + + .. py:attribute:: argspec + + Argument type specification provided to the constructor + + .. py:attribute:: legacy_nodeoptarg_nodeargs + + A tuple `(nodeoptarg, nodeargs)` that should be exposed as properties in + :py:class:`~pylatexenc.latexwalker.LatexMacroNode` to provide (as best as + possible) compatibility with pylatexenc < 2. + + This is either `(<1st optional arg node>, )` if + the first argument is optional and all remaining args are mandatory; or + it is `(None, )` for any other argument structure. + """ + + def __init__(self, argnlist=[], argspec="", **kwargs): + super(ParsedMacroArgs, self).__init__(**kwargs) + + self.argnlist = argnlist + self.argspec = argspec + + # for LatexMacroNode to provide some kind of compatibility with pylatexenc < 2 + self.legacy_nodeoptarg_nodeargs = self._get_legacy_attribs( + self.argspec, self.argnlist + ) + + def _get_legacy_attribs(self, argspec, argnlist): + nskip = 0 + while argspec.startswith("*"): + argspec = argspec[1:] + nskip += 1 + if argspec[0:1] == "[" and all(x == "{" for x in argspec[1:]): + return (argnlist[nskip], argnlist[nskip + 1 :]) + else: + return (None, argnlist) + + def to_json_object(self): + r""" + Called when we export the node structure to JSON when running latexwalker in + command-line. + + Return a representation of the current parsed arguments in an object, + typically a dictionary, that can easily be exported to JSON. The object + may contain latex nodes and other parsed-argument objects, as we use a + custom JSON encoder that understands these types. + + Subclasses may + """ + + return dict( + argspec=self.argspec, + argnlist=self.argnlist, + ) + + def __repr__(self): + return "{}(argspec={!r}, argnlist={!r})".format( + self.__class__.__name__, self.argspec, self.argnlist + ) + + +class MacroStandardArgsParser(object): + r""" + Parses the arguments to a LaTeX macro. + + This class parses a simple macro argument specification with a specified + arrangement of optional and mandatory arguments. + + This class also serves as base class for more advanced argument parsers + (e.g. for a ``\verb+...+`` macro argument parser). In such cases, + subclasses should attempt to provide the most suitable `argspec` (and + `argnlist` for the corresponding :py:class:`ParsedMacroArgs`) for their use, + if appropriate, or set them to `None`. + + Arguments: + + - `argspec`: must be a string in which each character corresponds to an + argument. The character '{' represents a mandatory argument (single + token or LaTeX group) and the character '[' denotes an optional argument + delimited by braces. The character '\*' denotes a possible star char at + that position in the argument list, a corresponding + ``latexwalker.LatexCharsNode('*')`` (or `None` if no star) will be + inserted in the argument node list. For instance, the string '\*{[[{' + would be suitable to specify the signature of the '\\newcommand' macro. + + Currently, the argspec string may only contain the characters '\*', '{' + and '['. + + The `argspec` may also be `None`, which is the same as specifying an + empty string. + + - `optional_arg_no_space`: If set to `True`, then an optional argument + cannot have any whitespace between the preceeding tokens and the '[' + character. Set this to `True` in cases such as for ``\\`` in AMS-math + environments, where AMS apparently introduced a patch to prevent a + bracket on a new line after ``\\`` from being interpreted as the + optional argument to ``\\``. + + - `args_math_mode`: Either `None`, or a list of the same length as + `argspec`. If a list is given, then each item must be `True`, `False`, + or `None`. The corresponding argument (cf. `argspec`) is then + respectively parsed in math mode (`True`), in text mode (`False`), or + with the mode unchanged (`None`). If `args_math_mode` is `None`, then + all arguments are parsed in the same mode as the current mode. + + - additional unrecognized keyword arguments are passed on to superclasses + in case of multiple inheritance + + Attributes: + + .. py:attribute:: argspec + + Argument type specification provided to the constructor. + + .. py:attribute:: optional_arg_no_space + + See the corresponding constructor argument. + + .. py:attribute:: args_math_mode + + See the corresponding constructor argument. + """ + + def __init__( + self, argspec=None, optional_arg_no_space=False, args_math_mode=None, **kwargs + ): + super(MacroStandardArgsParser, self).__init__(**kwargs) + self.argspec = argspec if argspec else "" + self.optional_arg_no_space = optional_arg_no_space + self.args_math_mode = args_math_mode + # catch bugs, make sure that argspec is a string with only accepted chars + if not isinstance(self.argspec, _basestring) or not all( + x in "*[{" for x in self.argspec + ): + raise TypeError( + "argspec must be a string containing chars '*', '[', '{{' only: {!r}".format( + self.argspec + ) + ) + # non-documented attribute that makes us ignore any leading '*'. We use + # this to emulate pylatexenc 1.x behavior when using the MacrosDef() + # function explicitly + self._like_pylatexenc1x_ignore_leading_star = False + + def parse_args(self, w, pos, parsing_state=None): + r""" + Parse the arguments encountered at position `pos` in the + :py:class:`~pylatexenc.latexwalker.LatexWalker` instance `w`. + + You may override this function to provide custom parsing of complicated + macro arguments (say, ``\verb+...+``). The method will be called by + keyword arguments, so the argument names should not be altered. + + The argument `w` is the :py:class:`pylatexenc.latexwalker.LatexWalker` + object that is currently parsing LaTeX code. You can call methods like + `w.get_goken()`, `w.get_latex_expression()` etc., to parse and read + arguments. + + The argument `parsing_state` is the current parsing state in the + :py:class:`~pylatexenc.latexwalker.LatexWalker` (e.g., are we currently + in math mode?). See doc for + :py:class:`~pylatexenc.latexwalker.ParsingState`. + + This function should return a tuple `(argd, pos, len)` where: + + - `argd` is a :py:class:`ParsedMacroArgs` instance, or an instance of a + subclass of :py:class:`ParsedMacroArgs`. The base `parse_args()` + provided here returns a :py:class:`ParsedMacroArgs` instance. + + - `pos` is the position of the first parsed content. It should be the + same as the `pos` argument, except if there is whitespace at that + position in which case the returned `pos` would have to be the + position where the argument contents start. + + - `len` is the length of the parsed expression. You will probably want + to continue parsing stuff at the index `pos+len` in the string. + """ + + from .. import latexwalker + + if parsing_state is None: + parsing_state = w.make_parsing_state() + + argnlist = [] + + if self.args_math_mode is not None and len(self.args_math_mode) != len( + self.argspec + ): + raise ValueError( + "Invalid args_math_mode={!r} for argspec={!r}!".format( + self.args_math_mode, self.argspec + ) + ) + + def get_inner_parsing_state(j): + if self.args_math_mode is None: + return parsing_state + amm = self.args_math_mode[j] + if amm is None or amm == parsing_state.in_math_mode: + return parsing_state + if amm == True: + return parsing_state.sub_context(in_math_mode=True) + return parsing_state.sub_context(in_math_mode=False) + + p = pos + + if self._like_pylatexenc1x_ignore_leading_star: + # ignore any leading '*' character + tok = w.get_token(p) + if tok.tok == "char" and tok.arg == "*": + p = tok.pos + tok.len + + for j, argt in enumerate(self.argspec): + if argt == "{": + (node, np, nl) = w.get_latex_expression( + p, strict_braces=False, parsing_state=get_inner_parsing_state(j) + ) + p = np + nl + argnlist.append(node) + + elif argt == "[": + + if self.optional_arg_no_space and p < len(w.s) and w.s[p].isspace(): + # don't try to read optional arg, we don't allow space + argnlist.append(None) + continue + + optarginfotuple = w.get_latex_maybe_optional_arg( + p, parsing_state=get_inner_parsing_state(j) + ) + if optarginfotuple is None: + argnlist.append(None) + continue + (node, np, nl) = optarginfotuple + p = np + nl + argnlist.append(node) + + elif argt == "*": + # possible star. + tok = w.get_token(p) + if tok.tok == "char" and tok.arg.startswith("*"): + # has star + argnlist.append( + w.make_node( + latexwalker.LatexCharsNode, + parsing_state=get_inner_parsing_state(j), + chars="*", + pos=tok.pos, + len=1, + ) + ) + p = tok.pos + 1 + else: + argnlist.append(None) + + else: + raise LatexWalkerError( + "Unknown macro argument kind for macro: {!r}".format(argt) + ) + + parsed = ParsedMacroArgs( + argspec=self.argspec, + argnlist=argnlist, + ) + + return (parsed, pos, p - pos) + + def __repr__(self): + return ( + "{}(argspec={!r}, optional_arg_no_space={!r}, args_math_mode={!r})".format( + self.__class__.__name__, + self.argspec, + self.optional_arg_no_space, + self.args_math_mode, + ) + ) + + +class ParsedVerbatimArgs(ParsedMacroArgs): + r""" + Parsed representation of arguments to LaTeX verbatim constructs, such as + ``\begin{verbatim}...\end{verbatim}`` or ``\verb|...|``. + + Instances of `ParsedVerbatimArgs` are returned by the args parser + :py:class:`VerbatimArgsParser`. + + Arguments: + + - `verbatim_chars_node` --- a properly initialized + :py:class:`pylatexenc.latexwalker.LatexCharsNode` that stores the + verbatim text provided. It is used to initialize the base class + :py:class:`ParsedMacroArgs` to expose a single mandatory argument with + the given verbatim text. The `verbatim_text` attribute is initialized + from this node, too. + + - `verbatim_delimiters` --- a 2-item tuple of characters used to delimit + the verbatim arguemnt (in case of a ``\verb+...+`` macro) or `None`. + + Attributes: + + .. py:attribute:: verbatim_text + + The verbatim text that was provided + + .. py:attribute:: verbatim_delimiters + + If the verbatim text was specified as an argument to ``\verb$...$``, then + this is set to a 2-item tuple that specifies the begin and end + delimiters. Otherwise, the attribute is `None`. + """ + + def __init__(self, verbatim_chars_node, verbatim_delimiters=None, **kwargs): + + # provide argspec/argnlist to the parent class so that any code that is + # not "verbatim environment-aware" sees this simply as the argument to + # an empty verbatim environment + super(ParsedVerbatimArgs, self).__init__( + argspec="{", argnlist=[verbatim_chars_node], **kwargs + ) + + self.verbatim_text = verbatim_chars_node.chars + self.verbatim_delimiters = verbatim_delimiters + + def __repr__(self): + return "{}(verbatim_text={!r}, verbatim_delimiters={!r})".format( + self.__class__.__name__, self.verbatim_text, self.verbatim_delimiters + ) + + +class VerbatimArgsParser(MacroStandardArgsParser): + r""" + Parses the arguments to various LaTeX "verbatim" constructs such as + ``\begin{verbatim}...\end{verbatim}`` environment or ``\verb+...+``. + + This class also serves to illustrate how to write custom parsers for + complicated macro arguments. See also :py:class:`MacroStandardArgsParser`. + + Arguments: + + .. py:attribute:: verbatim_arg_type + + One of 'verbatim-environment' or 'verb-macro'. + """ + + def __init__(self, verbatim_arg_type, **kwargs): + super(VerbatimArgsParser, self).__init__(argspec="{", **kwargs) + self.verbatim_arg_type = verbatim_arg_type + + def parse_args(self, w, pos, parsing_state=None): + + from .. import latexwalker + + if self.verbatim_arg_type == "verbatim-environment": + # simply scan the string until we find '\end{verbatim}'. That's + # exactly how LaTeX processes it. + endverbpos = w.s.find(r"\end{verbatim}", pos) + if endverbpos == -1: + raise latexwalker.LatexWalkerParseError( + s=w.s, pos=pos, msg=r"Cannot find matching \end{verbatim}" + ) + # do NOT include the "\end{verbatim}", latexwalker will expect to + # see it: + len_ = endverbpos - pos + + argd = ParsedVerbatimArgs( + verbatim_chars_node=w.make_node( + latexwalker.LatexCharsNode, + parsing_state=parsing_state, + chars=w.s[pos : pos + len_], + pos=pos, + len=len_, + ) + ) + return (argd, pos, len_) + + if self.verbatim_arg_type == "verb-macro": + # read the next nonwhitespace char. This is the delimiter of the + # argument + while w.s[pos].isspace(): + pos += 1 + if pos >= len(w.s): + raise latexwalker.LatexWalkerParseError( + s=w.s, pos=pos, msg=r"Missing argument to \verb command" + ) + verbdelimchar = w.s[pos] + beginpos = pos + 1 + endpos = w.s.find(verbdelimchar, beginpos) + if endpos == -1: + raise latexwalker.LatexWalkerParseError( + s=w.s, + pos=pos, + msg=r"End of stream reached while reading argument to \verb command", + ) + + verbarg = w.s[beginpos:endpos] + + argd = ParsedVerbatimArgs( + verbatim_chars_node=w.make_node( + latexwalker.LatexCharsNode, + parsing_state=parsing_state, + chars=verbarg, + pos=beginpos, + len=endpos - beginpos, + ), + verbatim_delimiters=(verbdelimchar, verbdelimchar), + ) + + return (argd, pos, endpos + 1 - pos) # include delimiters in pos/len + + def __repr__(self): + return "{}(verbatim_arg_type={!r})".format( + self.__class__.__name__, self.verbatim_arg_type + ) diff --git a/good_ai/src/sus/external/pylatexenc/version.py b/good_ai/src/sus/external/pylatexenc/version.py new file mode 100644 index 0000000..e2533d1 --- /dev/null +++ b/good_ai/src/sus/external/pylatexenc/version.py @@ -0,0 +1,58 @@ +# +# The MIT License (MIT) +# +# Copyright (c) 2021 Philippe Faist +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + + +# +# Self-note: Checklist +# +# 1) First some checks: +# +# - Set below in this file ' version_str = "X.Xb" ' (beta version for next +# release) for the following tests. +# +# - tests pass: https://travis-ci.org/github/phfaist/pylatexenc +# +# - LGTM looks good: https://lgtm.com/projects/g/phfaist/pylatexenc/ +# +# - python package creation works: (python setup.py sdist, pip install +# dist/pylatexenc-xxx.tar.gz) +# +# 2) update change log (doc/changes.rst) +# +# 3) bump version number here +# +# 4) git commit any remaining changes +# +# 5) " git tag vX.X -am '' " +# +# 6) " git push && git push --tags " +# +# 7) on github.com, fill in release details with a summary of changes etc. +# +# 8) create the source package for PyPI (" python3 setup.py sdist ") +# +# 8) upload package to PyPI (twine upload dist/pylatexenc-X.X.tar.gz -r realpypi) +# + +version_str = "2.10" diff --git a/good_ai/src/sus/get_sentences.py b/good_ai/src/sus/get_sentences.py new file mode 100644 index 0000000..8a0844d --- /dev/null +++ b/good_ai/src/sus/get_sentences.py @@ -0,0 +1,18 @@ +from typing import List + +from .data import sentence_ending_punctuations +from .nlp import nlp + + +def get_sentences(text: str, ignore_partial: bool = False) -> List[str]: + doc = nlp(text) + possible_sentences = [s.text for s in doc.sents] + + if ignore_partial: + possible_sentences = [ + s + for s in possible_sentences + if s[0].isupper() and s[-1] in sentence_ending_punctuations + ] + + return possible_sentences diff --git a/good_ai/src/sus/language/__init__.py b/good_ai/src/sus/language/__init__.py new file mode 100644 index 0000000..1ec3102 --- /dev/null +++ b/good_ai/src/sus/language/__init__.py @@ -0,0 +1,3 @@ +from .english_name_of_language import english_name_of_language +from .is_english import is_english +from .predict_language import predict_language diff --git a/good_ai/src/sus/language/english_name_of_language.py b/good_ai/src/sus/language/english_name_of_language.py new file mode 100644 index 0000000..9019c4d --- /dev/null +++ b/good_ai/src/sus/language/english_name_of_language.py @@ -0,0 +1,10 @@ +from typing import Optional + +from langcodes import Language + + +def english_name_of_language(language_code: Optional[str]) -> str: + if not language_code: + language_code = "und" + + return Language.get(language_code).display_name() diff --git a/good_ai/src/sus/language/is_english.py b/good_ai/src/sus/language/is_english.py new file mode 100644 index 0000000..24fb77c --- /dev/null +++ b/good_ai/src/sus/language/is_english.py @@ -0,0 +1,11 @@ +from typing import Optional + +from langcodes import standardize_tag, tag_distance + + +def is_english(language_code: Optional[str]) -> bool: + if not language_code: + language_code = "und" + + language_code = standardize_tag(language_code) + return tag_distance(language_code, "en") < 15 diff --git a/good_ai/src/sus/language/predict_language.py b/good_ai/src/sus/language/predict_language.py new file mode 100644 index 0000000..1fc3955 --- /dev/null +++ b/good_ai/src/sus/language/predict_language.py @@ -0,0 +1,16 @@ +from typing import Optional + +from langcodes import Language +from langdetect import LangDetectException, detect + + +def predict_language(text: Optional[str]) -> str: + if not text: + return Language.make().to_tag() + + try: + language_code = detect(text) + except LangDetectException: + return Language.make().to_tag() + + return Language.get(language_code).to_tag() diff --git a/good_ai/src/sus/lemmatize_text.py b/good_ai/src/sus/lemmatize_text.py new file mode 100644 index 0000000..bcf0d0f --- /dev/null +++ b/good_ai/src/sus/lemmatize_text.py @@ -0,0 +1,21 @@ +from typing import List + +from .lemmatize_token import lemmatize_token +from .nlp import nlp + + +def lemmatize_text( + text: str, + add_negation: bool = False, + add_part_of_speech: bool = False, +) -> List[str]: + doc = nlp(text) + + return [ + lemmatize_token( + t, + add_negation=add_negation, + add_part_of_speech=add_part_of_speech, + ) + for t in doc + ] diff --git a/good_ai/src/sus/lemmatize_token.py b/good_ai/src/sus/lemmatize_token.py new file mode 100644 index 0000000..f7a7ab8 --- /dev/null +++ b/good_ai/src/sus/lemmatize_token.py @@ -0,0 +1,20 @@ +from spacy.tokens import Token + +from .data import american_spellings + + +def lemmatize_token( + token: Token, + add_negation: bool = False, + add_part_of_speech: bool = False, +) -> str: + lemma = token.lemma_.lower() + + lemma = american_spellings.get(lemma, lemma) + + if add_part_of_speech: + lemma = f"{lemma}_{token.pos_}" + if add_negation and token._.negex: + lemma = f"NOT_{lemma}" + + return lemma diff --git a/good_ai/src/sus/match_names/__init__.py b/good_ai/src/sus/match_names/__init__.py new file mode 100644 index 0000000..d7bc1d1 --- /dev/null +++ b/good_ai/src/sus/match_names/__init__.py @@ -0,0 +1 @@ +from .match_names import get_name_match_score, get_name_parts, match_names diff --git a/good_ai/src/sus/match_names/config.py b/good_ai/src/sus/match_names/config.py new file mode 100644 index 0000000..6c99041 --- /dev/null +++ b/good_ai/src/sus/match_names/config.py @@ -0,0 +1,5 @@ +last_name_weight = 1.5 +first_name_weight = 0.9 +infixes_weight = 0.5 +initials_weight = 0.6 +match_threshold = 1.51 diff --git a/good_ai/src/sus/match_names/match_names.py b/good_ai/src/sus/match_names/match_names.py new file mode 100644 index 0000000..ffc0197 --- /dev/null +++ b/good_ai/src/sus/match_names/match_names.py @@ -0,0 +1,162 @@ +import functools +import re +from typing import List, Optional, Tuple + +from ..clean import clean +from .config import ( + first_name_weight, + infixes_weight, + initials_weight, + last_name_weight, + match_threshold, +) +from .name_parts import NameParts + + +def match_names(n1: Optional[str], n2: Optional[str]) -> bool: + score = get_name_match_score(n1, n2) + return score > match_threshold + + +def get_name_match_score(n1: Optional[str], n2: Optional[str]) -> float: + if not n1 or not n2: + return 0 + + p1 = get_name_parts(n1) + p2 = get_name_parts(n2) + + for f1 in p1.first_names: + for f2 in p2.first_names: + if f1[0] == f2[0]: + p1.initials = [i for i in p1.initials if i != f1[0]] + p2.initials = [i for i in p2.initials if i != f1[0]] + + last_name_score = last_name_weight * _match_percentage(p1.last_names, p2.last_names) + first_name_score = first_name_weight * _match_percentage( + p1.first_names, p2.first_names + ) + initials_score = initials_weight * _match_percentage(p1.initials, p2.initials) + infixes_score = min(0, infixes_weight * _match_percentage(p1.infixes, p2.infixes)) + + return last_name_score + first_name_score + initials_score + infixes_score + + +def get_name_parts(name: str) -> NameParts: + result = _get_name_parts(name) + return result.copy() + + +@functools.lru_cache(maxsize=None) +def _get_name_parts(name: str) -> NameParts: + name = _clean_name(name) + + first_names, name = _get_parenthesized_first_names(name) + initials, name = _get_initials(name) + + last_names = [] + infixes, name = _get_infixes(name) + if "," in name: + [last_name, *rest] = name.split(",") + rest_combined = " ".join(rest) + last_names.extend(last_name.split(" ")) + first_names.extend(rest_combined.split(" ")) + else: + parts = name.strip().split(" ") + if parts: + last_names.append(parts[-1]) + parts.pop(-1) + first_names.extend(parts) + else: + last_names.append(name) + + first_names = [f for f in first_names if f] + for f in first_names: + if all(f[0] != i for i in initials): + initials.append(f[0]) + + return NameParts( + first_names=first_names, + initials=initials, + infixes=infixes, + last_names=[ + name + for last_name in last_names + for name in (last_name.split("-") if "-" in last_name else [last_name]) + if name + ], + ) + + +def _clean_name(name: str) -> str: + name = clean(name, convert_to_ascii=True) + + name = re.sub( + r""" + (MD|PhD|Ir|dr|Dr|DR|MSc|MA|BSc|BA|Prof) + [,.]? + [ ]* + """, + "", + name, + flags=re.VERBOSE, + ) + + subs_made = 1 + while subs_made: + name, subs_made = re.subn(r"([A-Z]\.)\s+([A-Z])\b", r"\g<1>\g<2>", name) + + name = name.replace(r"\s+-\s+", "-") + name = " ".join(p for p in name.split(" ") if p) + name = re.sub(r"^([^,.]+) ([A-Z])$", r"\g<1>, \g<2>.", name) + return name + + +def _get_parenthesized_first_names(name: str) -> Tuple[List[str], str]: + groups = re.search(r"\(([a-zA-Z ]+)\)", name) + if groups: + return ( + groups.group(1).split(" "), + _remove_between_indices(name, groups.start(), groups.end()), + ) + + return [], name + + +def _get_initials(n: str) -> Tuple[List[str], str]: + initials = [] + + groups = re.search(r"\b(?P([A-Z]\.)*)(?P[A-Z])(\.|$)", n) + if groups: + first_name_abbreviations = [ + *groups.group("abr1").split("."), + groups.group("abr2"), + ] + first_name_abbreviations = [f for f in first_name_abbreviations if f != ""] + initials.extend(first_name_abbreviations) + n = _remove_between_indices(n, groups.start(), groups.end()) + + return initials, n + + +def _get_infixes(n: str) -> Tuple[List[str], str]: + infixes = ["de", "van", "der", "den", "te", "ter", "ten"] + + parts = n.split(" ") + return [p for p in parts if p.lower() in infixes], " ".join( + p for p in parts if p.lower() not in infixes + ) + + +def _remove_between_indices(s: str, start: int, end: int) -> str: + return s[:start] + s[end:] + + +def _match_percentage(l1: List[str], l2: List[str]) -> float: + if not l1 or not l2: + return 0 + + s1 = set(l1) + s2 = set(l2) + common_q = 2 * len(s1 & s2) / (len(s1) + len(s2)) + different_q = min(len(s1 - s2) / len(s1), len(s2 - s1) / len(s2)) + return common_q - different_q diff --git a/good_ai/src/sus/match_names/name_parts.py b/good_ai/src/sus/match_names/name_parts.py new file mode 100644 index 0000000..0d4c938 --- /dev/null +++ b/good_ai/src/sus/match_names/name_parts.py @@ -0,0 +1,10 @@ +from typing import List + +from pydantic import BaseModel + + +class NameParts(BaseModel): + first_names: List[str] + initials: List[str] + infixes: List[str] + last_names: List[str] diff --git a/good_ai/src/sus/nlp.py b/good_ai/src/sus/nlp.py new file mode 100644 index 0000000..582d96f --- /dev/null +++ b/good_ai/src/sus/nlp.py @@ -0,0 +1,21 @@ +try: + import en_core_web_lg +except ImportError: + import subprocess + + print("Spacy model en_core_web_lg not found locally, downloading...") + + subprocess.call( + [ + "pip", + "install", + "https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.2.0/en_core_web_lg-3.2.0-py3-none-any.whl", + ] + ) + import en_core_web_lg + +from .external.negspacy import negation # noqa: F401 it's important to import this + +nlp = en_core_web_lg.load() + +nlp.add_pipe("negex") diff --git a/good_ai/src/sus/parallel_map.py b/good_ai/src/sus/parallel_map.py new file mode 100644 index 0000000..dbc4ad1 --- /dev/null +++ b/good_ai/src/sus/parallel_map.py @@ -0,0 +1,34 @@ +from math import ceil +from typing import Any, Callable, Iterable, List, Optional + +import multiprocess as mp +import psutil +from tqdm.auto import tqdm + + +def parallel_map( + function: Callable[[Any], Any], + values: Iterable[Any], + chunk_size: Optional[int] = None, + concurrency: int = psutil.cpu_count(), + disable_progress: bool = False, +) -> List[Any]: + assert concurrency > 0 + assert chunk_size is None or chunk_size > 0 + + values = list(values) + + if not chunk_size: + chunk_size = max(1, ceil(len(values) / concurrency / 10)) + + if concurrency == 1: + iterable = values if disable_progress else tqdm(values) + return [function(v) for v in iterable] + + with mp.Pool(processes=concurrency) as pool: + if disable_progress: + return pool.map(function, values, chunksize=chunk_size) + + return list( + tqdm(pool.imap(function, values, chunksize=chunk_size), total=len(values)) + ) diff --git a/good_ai/src/sus/publication_tei/__init__.py b/good_ai/src/sus/publication_tei/__init__.py new file mode 100644 index 0000000..dc85fcf --- /dev/null +++ b/good_ai/src/sus/publication_tei/__init__.py @@ -0,0 +1,2 @@ +from .models import * +from .publication_tei import PublicationTEI diff --git a/good_ai/src/sus/publication_tei/models/__init__.py b/good_ai/src/sus/publication_tei/models/__init__.py new file mode 100644 index 0000000..38436f0 --- /dev/null +++ b/good_ai/src/sus/publication_tei/models/__init__.py @@ -0,0 +1,5 @@ +from .affiliation import Affiliation +from .author import Author +from .element import Element, Meta, MetaType, Paragraph, Title +from .publication_metadata import PublicationMetadata +from .text import Text diff --git a/good_ai/src/sus/publication_tei/models/affiliation.py b/good_ai/src/sus/publication_tei/models/affiliation.py new file mode 100644 index 0000000..66f2188 --- /dev/null +++ b/good_ai/src/sus/publication_tei/models/affiliation.py @@ -0,0 +1,11 @@ +from typing import Optional, Tuple + +from pydantic import BaseModel + + +class Affiliation(BaseModel): + institutions: Tuple[str, ...] + departments: Tuple[str, ...] + laboratories: Tuple[str, ...] + country: Optional[str] + settlement: Optional[str] diff --git a/good_ai/src/sus/publication_tei/models/author.py b/good_ai/src/sus/publication_tei/models/author.py new file mode 100644 index 0000000..d31ebe2 --- /dev/null +++ b/good_ai/src/sus/publication_tei/models/author.py @@ -0,0 +1,14 @@ +from typing import List, Optional + +from pydantic import BaseModel + +from .affiliation import Affiliation + + +class Author(BaseModel): + name: Optional[str] + orcid: Optional[str] + email: Optional[str] + corresponding: bool + affiliations: List[Affiliation] + coordinates: Optional[str] diff --git a/good_ai/src/sus/publication_tei/models/element.py b/good_ai/src/sus/publication_tei/models/element.py new file mode 100644 index 0000000..f942d5a --- /dev/null +++ b/good_ai/src/sus/publication_tei/models/element.py @@ -0,0 +1,30 @@ +from typing import List, Literal, Union + +from pydantic import BaseModel + +from .text import Text + + +class Title(BaseModel): + text: Text + + +class Paragraph(BaseModel): + sentences: List[Text] + + +MetaType = Literal[ + "abstract_start", + "abstract_end", + "acknowledgements_start", + "acknowledgements_end", + "annex_start", + "annex_end", +] + + +class Meta(BaseModel): + meta_type: MetaType + + +Element = Union[Title, Paragraph, Meta] diff --git a/good_ai/src/sus/publication_tei/models/publication_metadata.py b/good_ai/src/sus/publication_tei/models/publication_metadata.py new file mode 100644 index 0000000..904bbf5 --- /dev/null +++ b/good_ai/src/sus/publication_tei/models/publication_metadata.py @@ -0,0 +1,14 @@ +from typing import List, Optional + +from pydantic import BaseModel + + +class PublicationMetadata(BaseModel): + language: Optional[str] + title: Optional[str] + publisher: Optional[str] + doi: Optional[str] + md5: Optional[str] + publication_date: Optional[str] + keywords: List[str] + reference_count: int diff --git a/good_ai/src/sus/publication_tei/models/text.py b/good_ai/src/sus/publication_tei/models/text.py new file mode 100644 index 0000000..1046012 --- /dev/null +++ b/good_ai/src/sus/publication_tei/models/text.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class Text(BaseModel): + content: str + document_order: int + coordinates: str diff --git a/good_ai/src/sus/publication_tei/publication_tei.py b/good_ai/src/sus/publication_tei/publication_tei.py new file mode 100644 index 0000000..44d79ab --- /dev/null +++ b/good_ai/src/sus/publication_tei/publication_tei.py @@ -0,0 +1,176 @@ +from functools import cached_property +from typing import Any, List, Optional, Union + +from bs4 import BeautifulSoup +from bs4.element import NavigableString, Tag +from sus.publication_tei.models.element import Paragraph + +from ..clean import clean +from .models import Affiliation, Author, Element, Meta, PublicationMetadata, Text, Title + + +class PublicationTEI: + def __init__(self, tei: str): + self._document_order_counter = 0 + + if tei: + cleaned_xml = clean(tei, ignore_xml=True) + self.soup = BeautifulSoup(cleaned_xml, "xml") + else: + self.soup = BeautifulSoup() + + @cached_property + def publication_metadata(self) -> PublicationMetadata: + publication_date = ( + self.soup.publicationStmt.date.get("when") + if self.soup.publicationStmt and self.soup.publicationStmt.date + else None + ) + + keywords = ( + [self._element_to_text(k) for k in self.soup.keywords.find_all("term")] + if self.soup.keywords + else [] + ) + + return PublicationMetadata( + language=self.soup.teiHeader.get("xml:lang") + if self.soup.teiHeader + else None, + title=self._element_to_text(self.soup.title), + publisher=self._element_to_text(self.soup.publisher), + doi=self._element_to_text(self.soup.find("idno", type="DOI")), + md5=self._element_to_text(self.soup.find("idno", type="MD5")), + publication_date=publication_date, + keywords=keywords, + reference_count=self.get_reference_count(), + ) + + def get_reference_count(self) -> int: + references = self.soup.find("div", {"type": "references"}) + + if not references: + return 0 + + return len(references.findAll("biblStruct")) + + @cached_property + def authors(self) -> List[Author]: + if not self.soup.analytic: + return [] + + return [ + self._parse_author(author) + for author in self.soup.analytic.find_all("author") + ] + + @cached_property + def content(self) -> List[Element]: + self._document_order_counter = 0 + return self._get_elements(self.soup) + + @cached_property + def sentences(self) -> List[Text]: + return [ + sentence + for element in self.content + if isinstance(element, Paragraph) + for sentence in element.sentences + ] + + def _parse_author(self, raw: Tag) -> Author: + return Author( + name=( + clean(" ".join(name.get_text() for name in raw.persName)) + if raw.persName + else None + ), + orcid=self._element_to_text(raw.find(attrs={"type": "ORCID"})), + email=self._element_to_text(raw.email), + corresponding=raw.get("role") == "corresp", + affiliations=[ + self._parse_affiliation(aff) for aff in raw.find_all("affiliation") + ], + coordinates=raw.persName.get("coords") if raw.persName else None, + ) + + def _parse_affiliation(self, raw: Tag) -> Affiliation: + return Affiliation( + institutions=[ + self._element_to_text(v) + for v in raw.find_all("orgName", attrs={"type": "institution"}) + ], + departments=[ + self._element_to_text(v) + for v in raw.find_all("orgName", attrs={"type": "department"}) + ], + laboratories=[ + self._element_to_text(v) + for v in raw.find_all("orgName", attrs={"type": "laboratory"}) + ], + country=self._element_to_text(raw.address.country) + if raw.address and raw.address.country + else None, + settlement=self._element_to_text(raw.address.settlement) + if raw.address and raw.address.settlement + else None, + ) + + def _get_elements(self, raw: Tag) -> List[Element]: + results: List[Element] = [] + + for r in raw.find_all(["abstract", "div", "head", "p"]): + if r.name == "abstract": + results.append(Meta(meta_type="abstract_start")) + results.extend(self._get_primitives(r)) + results.append(Meta(meta_type="abstract_end")) + elif r.name == "div" and r.get("type") == "acknowledgement": + results.append(Meta(meta_type="acknowledgements_start")) + results.extend(self._get_primitives(r)) + results.append(Meta(meta_type="acknowledgements_end")) + elif r.name == "div" and r.get("type") == "annex": + results.append(Meta(meta_type="annex_start")) + results.extend(self._get_primitives(r)) + results.append(Meta(meta_type="annex_end")) + elif not r.find_parents(["abstract", "div"]): + results.extend(self._get_primitives(r)) + + return results + + def _get_primitives(self, raw: Tag) -> List[Element]: + results: List[Element] = [] + + for r in raw.find_all(["head", "p"]): + if r.name == "head" and r.get("coords") and r.get_text(): + results.append(Title(text=self._parse_text(r))) + elif r.name == "p" and r.find_all("s"): + results.append( + Paragraph( + sentences=[ + self._parse_text(sentence) + for sentence in r.find_all("s") + if sentence.get_text() + ] + ) + ) + + return results + + def _element_to_text( + self, element: Optional[NavigableString], default: Any = None + ) -> Union[str, Any]: + return ( + clean(element.get_text(separator=" ", strip=True)) if element else default + ) + + def _parse_text(self, raw: Tag) -> Text: + return Text( + content=clean(raw.get_text()), + document_order=self._generate_document_order_id(), + coordinates=raw.get("coords"), + ) + + def _generate_document_order_id(self) -> int: + value = self._document_order_counter + self._document_order_counter += 1 + return value diff --git a/good_ai/src/sus/unique.py b/good_ai/src/sus/unique.py new file mode 100644 index 0000000..0f92f08 --- /dev/null +++ b/good_ai/src/sus/unique.py @@ -0,0 +1,16 @@ +from typing import Any, Callable, Iterable, List + + +def unique( + values: Iterable[Any], *, key: Callable[[Any], Any] = lambda v: v +) -> List[Any]: + """Only keep first occurrences and maintian order""" + + key_values = {} + for v in values: + k = key(v) + if k not in key_values: + # dicts maintin insertion order: https://mail.python.org/pipermail/python-dev/2017-December/151283.html + key_values[k] = v + + return list(key_values.values()) diff --git a/good_ai/tests/__init__.py b/good_ai/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/good_ai/tests/open_s3/__init__.py b/good_ai/tests/open_s3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/good_ai/tests/open_s3/test_human_readable_to_byte.py b/good_ai/tests/open_s3/test_human_readable_to_byte.py new file mode 100644 index 0000000..85689df --- /dev/null +++ b/good_ai/tests/open_s3/test_human_readable_to_byte.py @@ -0,0 +1,26 @@ +import unittest + +from src.open_s3.helper import human_readable_to_byte + + +class TestHumanReadableToByte(unittest.TestCase): + def test_simple_cases(self): + self.assertEqual(human_readable_to_byte("1KB"), 1024) + self.assertEqual(human_readable_to_byte("2KB"), 2048) + + def test_fractions(self): + self.assertEqual(human_readable_to_byte("0.5KB"), 512) + self.assertEqual(human_readable_to_byte("20.5KB"), 1024 * 20 + 512) + + def test_formating(self): + self.assertEqual(human_readable_to_byte(" 1MB"), 1024 * 1024) + self.assertEqual(human_readable_to_byte(" 2 MB"), 1024 * 1024 * 2) + self.assertEqual(human_readable_to_byte(" 4 MB "), 1024 * 1024 * 4) + self.assertEqual(human_readable_to_byte("8MB "), 1024 * 1024 * 8) + self.assertEqual(human_readable_to_byte(" 1.5 MB "), 1024 * 1024 * 1.5) + + def test_casing(self): + self.assertEqual(human_readable_to_byte("0.5GB"), 0.5 * 1024 * 1024 * 1024) + self.assertEqual(human_readable_to_byte("0.5gB"), 0.5 * 1024 * 1024 * 1024) + self.assertEqual(human_readable_to_byte("0.5Gb"), 0.5 * 1024 * 1024 * 1024) + self.assertEqual(human_readable_to_byte("0.5gb"), 0.5 * 1024 * 1024 * 1024) diff --git a/good_ai/tests/open_s3/test_large_file.py b/good_ai/tests/open_s3/test_large_file.py new file mode 100644 index 0000000..4901931 --- /dev/null +++ b/good_ai/tests/open_s3/test_large_file.py @@ -0,0 +1,130 @@ +from pathlib import Path +import unittest +from unittest.mock import Mock, create_autospec, patch + +import botocore.session + +PATH = Path(__file__).parent.resolve() + + +from src.open_s3 import LargeFile + +credentials = { + "aws_region_name": "your_region_like_eu-west-2", + "aws_access_key_id": "YOUR_ACCESS_KEY_ID", + "aws_secret_access_key": "YOUR_VERY_SECRET_ACCESS_KEY", + "large_files_bucket_name": "create_a_bucket_and_put_its_name_here", + "other_key": 23, + "endpoint_url": "this is optional, for backblaze, use this: https://s3.us-west-002.backblazeb2.com", +} + + +class TestLargeFile(unittest.TestCase): + def test_uninitialized(self): + self.assertRaises(ValueError, LargeFile, "test-file") + + def test_bad_file_modes(self): + self.assertRaises(ValueError, LargeFile, "test-file", "w", version=3) + self.assertRaises(ValueError, LargeFile, "test-file", "wb", version=3) + self.assertRaises(ValueError, LargeFile, "test-file", "w+r") + self.assertRaises(ValueError, LargeFile, "test-file", "test") + + @patch("botocore.session") + def test_initialized_with_dict(self, session): + session_mock = Mock() + session.get_session = create_autospec( + botocore.session.get_session, return_value=session_mock + ) + + s3 = Mock() + s3.list_objects_v2 = Mock( + return_value={ + "Contents": [ + { + "Key": "test-file/0", + "Size": 30, + }, + { + "Key": "test-file/1", + "Size": 300, + }, + { + "Key": "test-file/2", + "Size": 187, + }, + ] + } + ) + + session_mock.create_client = Mock(return_value=s3) + + LargeFile.configure_credentials(**credentials) + lf = LargeFile("test-file") + session_mock.set_credentials.assert_called_once_with( + access_key=credentials["aws_access_key_id"], + secret_key=credentials["aws_secret_access_key"], + ) + session_mock.create_client.assert_called_once_with( + "s3", + region_name=credentials["aws_region_name"], + endpoint_url=credentials["endpoint_url"], + ) + + s3.list_objects_v2.assert_called_once_with( + Bucket=credentials["large_files_bucket_name"], Prefix="test-file" + ) + + self.assertEqual(lf._version, 2) + self.assertEqual(lf._local_name, "test-file-2") + self.assertEqual(lf._s3_name, "test-file/2") + + @patch("botocore.session") + def test_initialized_with_file(self, session): + session_mock = Mock() + session.get_session = create_autospec( + botocore.session.get_session, return_value=session_mock + ) + + s3 = Mock() + s3.list_objects_v2 = Mock( + return_value={ + "Contents": [ + { + "Key": "test-file/0", + "Size": 30, + }, + { + "Key": "test-file/1", + "Size": 300, + }, + { + "Key": "test-file/2", + "Size": 187, + }, + ] + } + ) + + session_mock.create_client = Mock(return_value=s3) + + LargeFile.configure_credentials_from_file(PATH / "../../example_secrets.ini") + lf = LargeFile("test-file") + + session_mock.set_credentials.assert_called_once_with( + access_key=credentials["aws_access_key_id"], + secret_key=credentials["aws_secret_access_key"], + ) + session_mock.create_client.assert_called_once_with( + "s3", + region_name=credentials["aws_region_name"], + endpoint_url=credentials["endpoint_url"], + ) + + assert s3.list_objects_v2.called + s3.list_objects_v2.assert_called_once_with( + Bucket=credentials["large_files_bucket_name"], Prefix="test-file" + ) + + self.assertEqual(lf._version, 2) + self.assertEqual(lf._local_name, "test-file-2") + self.assertEqual(lf._s3_name, "test-file/2") diff --git a/good_ai/tests/sus/__init__.py b/good_ai/tests/sus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/good_ai/tests/sus/data/10.1136_bmjspcare-2021-003026.pdf.tei.xml b/good_ai/tests/sus/data/10.1136_bmjspcare-2021-003026.pdf.tei.xml new file mode 100644 index 0000000..df62e25 --- /dev/null +++ b/good_ai/tests/sus/data/10.1136_bmjspcare-2021-003026.pdf.tei.xml @@ -0,0 +1,1946 @@ + + + + + + Spiritual dimension in palliative medicine: a qualitative study of learning tasks: medical students, teachers, educationalists + + + BMJ + +

Copyright BMJ

+
+ 2021-07-20 +
+ + + + + + Ms + Jolien + Pieters + + j.pieters@maastrichtuniversity.nl + 0000-0002-9327-3977 + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + Daniëlle + Verstegen + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + Diana + Dolmans + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + Evelien + Neis + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + Franca + Warmenhoven + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + Marieke + Van Den Beuken-Van Everdingen + + + + + Centre of Expertise for Palliative Care, Maastricht UMC+, Maastricht, The Netherlands + + Centre of Expertise for Palliative Care + UMC+ +
+ Maastricht, Maastricht + The Netherlands +
+
+
+ + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, Maastricht University, Maastricht 6229 ER, The Netherlands; + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + Maastricht University +
+ 6229 ER + Maastricht + The Netherlands +
+
+
+ Spiritual dimension in palliative medicine: a qualitative study of learning tasks: medical students, teachers, educationalists +
+ + BMJ Supportive & Palliative Care + BMJ Support Palliat Care + 2045-435X + 2045-4368 + + BMJ + bmjspcare-2021-003026 + + + + EAE447B92E88C8C72D4610586A269870 + 10.1136/bmjspcare-2021-003026 + Received 14 March 2021 Accepted 1 June 2021 +
+
+
+ + + + GROBID - A machine learning software for extracting information from scholarly documents + + + + + + +
+

+ Background Palliative care is gaining importance within the physician's range of duties. + In the undergraduate medical curriculum, education on the four dimensions of care is insufficient. + The spiritual dimension is hardly addressed. + Therefore, we developed a coherent set of learning tasks targeted at learning to communicate about the spiritual dimension. + The learning tasks are based on educational principles of authentic learning, reflective learning and longitudinal integration in the curriculum. + This article reports on the feasibility of using these learning tasks in the medical curricula. + Methods Teachers and educational scientists were interviewed and students were asked to evaluate the learning tasks in focus groups. + Interview transcripts were analysed by three independent researchers. +

+
+
+ Results +

+ The learning tasks encourage the students to reflect on the four dimensions of palliative care and their personal values. + Learning was clearly organised around authentic learning tasks relevant to the later profession, using paper, video cases, as well as simulations and real patients. + Participants suggest giving more attention to cultural diversity. + As palliative care is an emotionally charged subject, the safety of both student and patient should be guaranteed. + All participants indicated that the program should start in the bachelor phase and most agreed that it should be integrated vertically and horizontally throughout the undergraduate program, although there is some debate about the optimal moment to start. + Conclusion The tasks, are authentic, encourage the students to reflect on the spiritual dimension of palliative care and are suitable for integration in the undergraduate medical curriculum. +

+
+
+ Key messages +

+ What was already known? + ► Insufficient education on four dimensions of palliative care in undergraduate medical curriculum. + ► Especially the spiritual dimension is hardly addressed. +

+
+
+ What are the new findings? +

+ ► A coherent set of learning tasks developed in line with instructional design guidelines. + ► Stakeholder evaluations positive; confirm that these are authentic and encourage reflection. +

+

+ What is their significance? + a. Clinical: Enhances palliative care education for medical students. + b. + Research: Suitable for integration in undergraduate medical curriculum. +

+

+ copyright. +

+
+
+
+
+ + + + + + + + + + + +
+ INTRODUCTION +

+ The need for palliative care is set to grow due to demographic changes, longer disease trajectories and higher comorbidity. + + Central to providing palliative care is the holistic, patient-centred and multidimensional approach, which addresses not only the physical, but also the psychological, social and spiritual dimension. + 1 + + + Providing palliative care is increasingly recognised as a universal responsibility of healthcare professionals + 2 3 + and all doctors will see patients with progressive life-limiting conditions at some point during their careers. + 4 + + Physicians, irrespective of specialism, should be both competent and confident in caring for the palliative care patient. + + Taking care of palliative care patients is typically associated with powerful and highly emotional situations affecting junior doctor's emotional well-being. + 5 + + + It is therefore important that Original research junior doctors develop the ability to guide palliative care patients during their medical training. + 6 + + + lthough there is a growing international movement to embed palliative care education in the undergraduate medical curricula, + 5 + this topic is not adequately addressed within all European medical universities. + + + Several studies demonstrate that medical students do not receive sufficient education in this area. + 7 8 + + + Students do not feel well prepared + [9] + [10] + [11] + [12] + and feel especially ill -prepared to raise and discuss the psychological, social and spiritual dimensions of care. + 13 + + + Their education primarily focuses on one dimension-the physicalwhile allowing the others to fall by the wayside. + 14 + + + tudents also report that self-care and reflection in the context of palliative care do not get much attention in their education. + 9 13 + + + In the Netherlands, the undergraduate medical education assigns only limited attention on palliative and end-of-life care. + 13 15-17 + + + This despite that the national competency framework states that the doctor should promote people's health and related quality of life, also in the palliative phase. + 18 + + + The competences that Dutch medical students need to acquire to provide good-quality palliative care have recently been set out in an educational framework. + 19 + + This framework specifies among others that the medical students should be able to talk to the patient and family about the incurable illness, prognosis and death, and discuss the four dimensions of care. + They should also be able to take care of their own well-being and reflect on their own spiritual needs, alongside their perceptions about life, death and dying. +

+

+ To bridge the gap between what students should learn and actually learn about spiritual care, we developed a coherent set of eight learning tasks. + Addressing the spiritual dimension is a complex task. + + According to current educational principles, learning complex tasks can be supported by providing authentic or realistic learning tasks + 20 + , by using principles of reflective learning, and should be integrated in the curricula. + + + Authentic tasks allow students to acquire knowledge, skills and attitudes in an integrated fashion, + 21 + which improves the transfer of the curriculum to the workplace. + 20 + + These authentic learning tasks can be interwoven in existing curricula in a horizontal and vertical integration manner. + + 3] + [24] + [25] + Through reflection, students are encouraged to think about their role as a physician, + 22 + foster professional growth, release the emotional burden of caring for palliative care patients and increase patient care skills. + 24 + + + Self-reflective training on the spiritual dimensions within the students' own lives is recommended. + 2 + + e developed a coherent set of realistic authentic learning tasks, in which students learn about and reflect on communication about the four dimensions of care, with a particular focus on the spiritual dimension. + The main aims of these learning tasks are that students learn about spiritual care, are able to talk about it with a palliative care patient, and to reflect on their spiritual experiences regarding life and death. + This article gives more insight into the usability and feasibility of these learning tasks from the stakeholders' perspectives, that is, medical students, teachers and educational scientists on the design of the learning tasks based on the educational principles of authentic educational scenarios, reflection and integration. + The research question is: How do medical students, teachers and educational scientists evaluate a set of coherent learning tasks focusing on the spiritual dimension of palliative care? +

+
+
+ METHODS +
+
+ Design +

+ Three groups of stakeholders were asked to participate in this evaluation: medical students, teachers, and educational scientists. + The students were interviewed in focus groups. + The teachers and educational scientists were questioned in individual interviews, due to their busy schedules. + This qualitative approach was used to gather in-depth information and insights from our stakeholders. +

+
+
+ Setting +

+ In the Netherlands, it takes 6 years to qualify as a physician. + In the first 3 years, the Bachelor's program, the student primarily acquires theory and medical knowledge. + In the last 3 years, the Master's program, the focus is on the application of knowledge in the work setting by letting students rotate between different internships. +

+
+
+ Coherent set of learning tasks +

+ + We designed a set of eight learning tasks (table + 1 + ; for a full description, see online supplemental appendix 1), designed to be integrated into the undergraduate medical curriculum. + + The designers included diversity and variations in teaching methods, diseases, treatment plans, age and gender of the patient. + The competencies to be acquired are described in box 1. + + These competences are a selection of the framework from Pieters et al. + 19 + The educational principles of authenticity, and reflection are incorporated into the set of learning tasks (see table + 2 + ). + +

+
+
+ Original research +
+
+ Participants +

+ Three groups of stakeholders were asked to participate in the evaluation: medical students, teachers and educational scientists. + The stakeholders came from faculties of medicine of four different universities in the Netherlands. +

+
+
+ Medical students (N=9) +

+ These stakeholders were asked to be interviewed in focus groups as they represented the educational users as learners. + Medical students in their final year of the Bachelor's program or studying for their Master's degree were invited. + These students have an informed opinion as to which tasks they deemed suitable for students and at what stage the tasks could best be implemented in the curriculum. +

+
+
+ Teachers of palliative care (N=9) +

+ These stakeholders were invited for their insight and experience in education and their substantive expertise in palliative care. + This group of stakeholders included medical specialists, mental healthcare providers and psychologists involved in teaching in undergraduate medical education. +

+

+ Educational scientists (N=4) +

+

+ These stakeholders were asked for their expertise in both educational design and the educational principles used in this learning program (Authentic learning, reflection and integration into existing courses). + The educational scientists worked at medical faculties. +

+
+
+ Instruments +

+ The teachers and educational scientists were interviewed individually, the students were interviewed in focus groups, using the same semistructured interview guide (see online supplemental appendix 2). + The interview guide asked for perceptions of the set of learning tasks, focusing on the educational learning principles that shaped them: authentic learning tasks, the principles of reflective learning, and the integration into the existing courses. +

+
+
+ Procedure +

+ Participants were recruited through purposeful sampling within the network of Pasemeco until data saturation was reached. + They were invited through an email that included the information letter. + Prior to their individual or focus group interview, the participants received a short explanatory video and a document with an overview of the set of learning tasks. + The interviews took no more than 45 min and were conducted by JP and EN. + The interviewees gave their written informed consent. +

+
+
+ Analysis +

+ The interviews were recorded and transcribed. + To support the qualitative data analysis, Atlas. + ti V.8 was used. + + The transcripts were analysed using template analysis, taking into account the step-by-step plan of Brooks et al. + 26 + Researchers JP, EN, and FW carried out the preliminary coding of the data, starting with determining the a priori themes. + + These themes were based on the components of the research question (authentic learning tasks; reflection and integration into the curriculum). + Two interview transcripts were read and coded independently by researchers JP, EN, and FW. + They discussed their findings, merged the themes into meaningful clusters, and provided an initial coding template. + This template was then applied independently by the three researchers to three other transcripts. +

+

+ After consultation, the template was finalised and applied to the full data set. + Themes were discussed and elaborated on iteratively with the whole research team. + JP and DV coded the other eight transcripts independently and discussed the results afterward. + The results were then discussed among all of the authors. +

+
+
+ Reflexivity +

+ This learning program was developed by a multidisciplinary group that included educational scientists (DV and DD), researchers (JP and EN) and physicians +

+
+
+ Box 1 : Intended competencies of the learning tasks +

+ Competencies: +

+

+ 1) The student communicates in a respectful and empathetic manner with patients and relatives. +

+

+ 2) The student makes the four dimensions (somatic, psychological, social and spiritual) of palliative care discussable with the patient and family. +

+

+ 3) The student reflects on distance and closeness in the treatment relationship with a palliative patient. + 4) The student reflects on his/her spiritual and existential views around life and death. +

+
+
+ RESULTS +
+
+ Authentic tasks +

+ The stakeholders agreed that the educational materials, including the case descriptions, the video clips and short movies, showed the students a realistic view of what they could encounter in the workplace. + The learning tasks were authentic and presented in a logical and structured order, from paper cases to a fullfledged conversation with a patient. + The stakeholders commented that the learning tasks and materials could be further improved if they provided a better reflection of the cultural diversity within the patient population. +

+

+ Students especially appreciated the clips in which real patients and doctors talked about palliative care and suggested that encouraging the possibility to discuss cases students have encountered in medical practice would further increase the authenticity. +

+
+
+ Reflective learning +

+ Stakeholders recognised reflection is a recurring element across the learning tasks. + They also consider it to be an important skill. + According to the stakeholders, the majority of the students should be able to reflect well on the various health dimensions, but the spiritual dimension can be emotionally charged. + Some stakeholders indicated that to be able to reflect on this properly, some life experiences would not go amiss. + Novice medical students' reflections lack the necessary depth in comparison with Master's degree students. + Both the teachers and students preferred oral group reflection. +

+

+ The students indicated that they attached great value to the interaction with their fellow students and that it helps them express themselves better. + Student safety must be ensured in order for them to properly reflect in a group. +

+
+
+ Integration +

+ The stakeholders believed that the learning tasks should be integrated across the curriculum in different years. + However, it is important that this does not become too fragmented and that students can clearly see the cohesion. + Almost all stakeholders mentioned that the initial tasks (tasks 1-5) would be best implemented in the Bachelor's program, as the cases in these tasks revolve around the basis of palliative care. + The other learning tasks (tasks 6-8) were considered more appropriate for more advanced students during the internships as these center around communication with a simulated or real patient. + According to the stakeholders, they required experience with patients that students only acquire during the Master's degree. + There was no consensus as to when to start the learning trajectory. + Some stakeholders believed that palliative care education should start in the very first year, so that students become immediately aware that palliative care is a part of routine care. + But others thought it better to wait until the end of the Bachelor's program, because students would not be open to the subject or not fully understand the subject matter until then. + The implementation should also take into account the differences between students. + In the final learning task, students conduct an unsupervised interview with a palliative patient. + According to the stakeholders, the learning program does prepare students well for this interview, but it is still an emotionally charged task. + In order to protect both the patients and the students, the students must be well prepared and be able to take the lead in such an interview. + + Some students may need more training or support (table + 3 + ). + +

+
+
+ DISCUSSION +

+ The medical students, teachers, and educational scientists agree that the set of learning tasks was designed in line with contemporary instructional design guidelines. + Learning was clearly organised around authentic learning tasks relevant to the later profession, using paper, video cases, as well as simulations and real patients. + The tasks encourage the students to reflect on the four dimensions of palliative care and their personal values. + There were various options offered for the integration of the tasks into existing curricula. + The stakeholders also offered a number of suggestions to take into account, which will be discussed further. +

+

+ First, palliative care and the spiritual dimension in particular are emotionally charged subjects. + Extra attention to both student and patient safety is required. + Both the students and teachers attached great value to the group discussions and reflections, where a bond of trust and psychological safety between all of the attendees was considered to be essential. + + These findings are in line with the literature, where confidentially sharing experiences is needed and only possible in a group of people who know each other. + 2 27 + Second, the educational materials were considered authentic with variation in cases and learning methods, but to increase authenticity, the stakeholders suggested including more cultural diversity among the patient cases used in the learning tasks. + + Contemporary society is more culturally diverse than the current materials reflected. + + It is an important topic that should not be overlooked, given that many physicians are unfamiliar with the specific needs of ethnic minorities regarding palliative care and communication. + 28 + + hird, there was discussion on how to integrate learning tasks or palliative care education in general. + All participants agreed that this should start in the Bachelors and most agreed that it should be integrated vertically, throughout the undergraduate program and not as a stand-alone module. + Furthermore, it needs to be integrated horizontally, where relevant, for instance, in cardiology, oncology, humanities, and copyright. +

+

+ on January 21, 2022 by guest. + Protected by http://spcare.bmj.com/ + Original research communication training. + Integrating this set of learning tasks, therefore, mainly depends on the curriculum of a medical university and its identity as to where and when palliative care education can be implemented in the curriculum. + + The idea of vertically and horizontally integration is in line with recent educational research and guidelines. + 4 29 30 + + ome stakeholders argued that for learning about spiritual and palliative care in general, students should have some life experience and, therefore, it would be more suitable to start the program later in the bachelor. + Others argue that palliative care should be normalised for the students and should, therefore, start in the first year. + + The concern that early exposure may be emotionally challenging for young students, is the main reason why curricula integrate palliative care education in general later in the program. + 31 32 + + + However, research shows that direct experience with palliative care for first-year students is associated with positive effects on the students' attitudes regarding caring for palliative care patients. + 29 31 33 34 + + + Junior doctors who were trained earlier in palliative care have enhanced competencies of psychosocial and spiritual aspects of palliative care, communication, and self-awareness. + 34 + + + Thus, early exposure helps to normalise death and dying and the complex emotions that students and physicians can encounter while treating palliative care patients. + 31 33 34 + + nterestingly, in this study, students themselves had lively discussions on this topic too, but they mainly discussed more practical obstacles: when is there time for this in the curriculum? +

+

+ This study had some limitations. + To begin with, we evaluated the set of learning tasks prior to its implementation. + This meant that we could not reflect on the implementation process itself, leaving this to be researched at a later stage. + We also focused on the stakeholders in the educational setting: medical students, teachers, and educational scientists. + Patients 'I would also pay more attention to a more diversified background,(…). + The impression here is that it might be more about the classic white Dutch native than perhaps the Surinamese or Moroccan who gets into these problems and needs an essentially different approach' -I7 +

+

+ '(the clips, eds.)in the sense that the doctors who were talking, for example a [person] I have met in the clinic as well so that appeals more to the imagination. + That actually makes it even more authentic' -FG2 +

+

+ 'That there are not only videos but also something like a link to a wishlist, and a Volkskrant [newspaper] article so I think that's already very authentic. + You might want to make it a little more authentic by having students contribute cases they encounter' -FG2 Reflective learning 'I believe, indeed, that in real life that might be better than on paper, especially because you sometimes have to put something on paper and then you are not really sure or you feel differently or have not enough time and if you are really working in such a group, then ideas emerge or feelings that other people then evoke in you or you hear it and you think, oh that's right, I agree or I do not' -FG1 'What is also important here is that in your educational groups, or in the subgroup where you would discuss this, you have a bond of trust as well as feel safe' -I11 +

+
+
+ Integration +

+ And what might also be good timing,(…), is the second half of the third year of medicine. + Students are then very receptive to everything that is useful in their internships, so to speak, because then it is kind of a big thing that you will have to tackle soon and so you have to get started' -FG2 +

+

+ 'I see no reason not to do it (integration as of year 1, eds.). + And another advantage is that students will get an idea of -oh, yes, so this is part of the job as well? + It soon makes it more normal, because students often enrol with a strong idea of: we are going to make everyone better… but accepting that you can't make someone better will become the reality… but it is not really part of the idea of beginning students so, in that sense, I think: maybe it's also good to create some kind of awareness here' -I12 +

+

+ 'I believe you could use that simulation interview as a go/no-go for having an interview with a patient. + And, once again, they really are vulnerable patients, often in the final stage of life, who are often quite willing to contribute to the training, but who should also not be burdened disproportionately by oafs or students who are not interested or cut corners, and that such a patient is about to go home with a bad feeling after 45 minutes' -I8 FG, Focus Group; I, Interview. + were not involved in the evaluation, although they were involved in the overarching project within which the learning tasks were developed. + Last but not least, the educational principles within this learning program were tailored to the Dutch situation. + The educational principles and general setup of the design are internationally transferable, if tailored to the national situation. + Following the authenticity principle means that it is important to paint a realistic picture of the professional field. + If authenticity is to be guaranteed, learning tasks will have to be reviewed and adapted to be used in other countries. + This also regards to the principle of reflective learning. + The students involved in this study are already familiar with reflecting and sharing their opinion, since it plays a vital role in their curriculum. + On a national level there is a lively debate on end-of-life care, but not on the spiritual dimension. + Therefore, an emphasis was placed on the spiritual dimension. +

+

+ This study has several practical implications. + First, it pays off to integrate the program horizontally and vertically into the curriculum. + It depends on the specific tasks and the curriculum itself how and where the integration can best take place. + Some tasks will fit in well with education regarding communication or where clinical conditions are discussed such as oncology or lung disease. + To avoid fragmentation and guarantee that all aspects are covered, however, it is also important that someone has an overview of where and how palliative care is addressed in the curriculum, for example, a curriculum coordinator or someone specifically assigned with this task. + Furthermore, the tasks can always be adapted to specific contexts. + For example, it might be difficult to arrange interviews with palliative care patients. + Students could then interview a chronically ill patient instead of a palliative patient. + With explicit attention to communication and spiritual care education, it is possible to better prepare students for working in the professional field. + The spiritual dimension of care deserves explicit attention in the medical curriculum. +

+
+
+ + + +
+

+ , 2022 by guest. + Protected by http://spcare.bmj.com/ + Original research +

+
+
+
+
+ Table 1 + + +
+

+ The eight learning tasks +

+
+
+ + + + copyright. + + + 1 + Learning about the four dimensions: physical, psychological, social + + + + and emotional + + + 2 + Discussing and reflecting on palliative patient in the context of life + + + 3 + Reflection on personal views on life, death and dying + + + 4 + Recognising the spiritual dimension + + + 5 + Learning to integrate the spiritual dimension into counselling + + + 6 + To conduct an interview on meaning and purpose and work with + + + + the diamond model + + + 7 + Simulation interview with a palliative patient + + + 8 + Unsupervised interview with a palliative patient + +
+ +

+ on January 21, 2022 by guest. + Protected by http://spcare.bmj.com/ +

+
+
+
+ Table 2 + + +
+

+ The incorporated educational principles in the different learning tasks +

+
+
+ + + + copyright. + + + + Authentic tasks Reflecting Integration + The materials provided are based on life-like, real cases encountered by professionals in their work. The learning tasks have been developed in close consultation with various care professionals. Furthermore, the student interviews a real patient they have met in the workplace. Reflection is a vital part of almost every learning task. Reflection is emphasised in several areas: ► Reflection on personal values, standards, and allocation of meaning and purpose (learning tasks 2, 3 and 5) ► Reflection on the influence of the four dimensions on the patient and the care trajectory (learning task 1, 4, 5 and 8) The learning tasks are developed for integration into existing courses. authentic tasks. The video clips used are based on + on January 21, 2022 by guest. Protected by http://spcare.bmj.com/ + +
+
+
+ Table 3 + + +
+

+ Themes and quotes of the stakeholders this(authenticity, eds.)in so far that patients tell you a lot of stories. + So, in that sense, they are all very authentic stories and this is how patients present themselves to doctors' -I10 'If you look at the learning tasks, you do see a clear structure. + From watching to an increasingly active role to eventually actually having a conversation with a patient, of course' -I8 +

+
+
+ + + Theme + Quotes + + + Authentic + 'I recognise + + + tasks + + +
+
+ BMJ Support Palliat Care: first published as 10.1136/bmjspcare-2021-003026 on 20 July 2021. Downloaded from + Pieters J, et al. BMJ Supportive & Palliative Care 2021;0:1-7. doi:10.1136/bmjspcare-2021-003026 + + + +
+
+ Acknowledgements +

+ We thank Judith Westen and Jimmy Frerejean for their support in designing these learning tasks. + We further thank the involved students, teachers and educational scientist. +

+
+
+ +
+
+

+ Contributors DV and FW developed the learning tasks; JP and EN conducted the data collection. + JP, EN and FW interpreted the data. + JP drafted the manuscript; DV, MvdBvE and DD. + supervised the study process. + All authors have read and agreed to the final version of the manuscript. +

+

+ Funding This work was supported by ZonMW (project number 80-84400-98-027). +

+
+
+ Competing interests None declared. +

+ Patient consent for publication Not required. +

+
+
+ Ethics approval Ethical clearance was obtained from the Netherlands Association for Medical Education Ethical Review +

+ Board (NVMO-ERB file 2020.5.3). + Written consent was obtained from all participants. + The participants gave their permission to use anonymised quotes. +

+

+ Provenance and peer review Not commissioned; externally peer reviewed. +

+
+
+ Data availability statement Data are available on reasonable request. +
+
+ Open access This is an open access article distributed in accordance with the Creative Commons Attribution Non +

+ Commercial (CC BY-NC 4.0) license, which permits others to distribute, remix, adapt, build upon this work noncommercially, and license their derivative works on different terms, provided the original work is properly cited, appropriate credit is given, any changes made indicated, and the use is noncommercial. + See: http:// creativecommons. + org/ licenses/ by-nc/ 4. 0/. +

+
+
+ ORCID iD +

+ Jolien Pieters http:// orcid. + org/ 0000-0002-9327-3977 +

+
+
+
+ + + + + + Palliative care: time for action + + + A + Al-Mahrezi + + + + + Al-Mandhari + + + + + Z + + + 10.5001/omj.2016.32 + + + Oman Med J + + 31 + + + + + Al-Mahrezi A, Al-Mandhari Z. Palliative care: time for action. Oman Med J 2016;31:161-3. + + + + + An EAPC white paper on multi-disciplinary education for spiritual care in palliative care + + + M + Best + + + + + C + Leget + + + + + A + Goodhead + + + 10.1186/s12904-019-0508-4 + + + BMC Palliat Care + + 19 + 9 + + + + Best M, Leget C, Goodhead A, et al. An EAPC white paper on multi-disciplinary education for spiritual care in palliative care. BMC Palliat Care 2020;19:9. + + + + + Palliative care in the global setting: ASCO resource-stratified practice guideline + + + H + Osman + + + + + S + Shrestha + + + + + S + Temin + + + + + Journal of global oncology + + 4 + + + + + Osman H, Shrestha S, Temin S. Palliative care in the global setting: ASCO resource-stratified practice guideline. Journal of global oncology 2018;4:1-24. + + + + + Teaching the difficultto-teach topics + + + A + Macpherson + + + + + I + Lawrie + + + + + S + Collins + + + 10.1136/bmjspcare-2012-000408 + + + BMJ Support Palliat Care + + 4 + + + + + MacPherson A, Lawrie I, Collins S, et al. Teaching the difficult- to-teach topics. BMJ Support Palliat Care 2014;4:87-91. + + + + + Palliative and end-of-life care and junior doctors': a systematic review and narrative synthesis + + + A + Bharmal + + + + + T + Morgan + + + + + I + Kuhn + + + 10.1136/bmjspcare-2019-001954 + doi:10.1136/ bmjspcare-2019-001954 + + + BMJ Support Palliat Care + + 0 + + + + Epub ahead of print: 13 Nov + Bharmal A, Morgan T, Kuhn I, et al. Palliative and end-of-life care and junior doctors': a systematic review and narrative synthesis. BMJ Support Palliat Care 2019;0. doi:10.1136/ bmjspcare-2019-001954. [Epub ahead of print: 13 Nov 2019]. + + + + + An important but stressful part of their future work: medical students' attitudes to palliative care throughout their course + + + S + Barclay + + + + + R + Whyte + + + + + P + Thiemann + + + 10.1016/j.jpainsymman.2014.06.004 + + + J Pain Symptom Manage + + 49 + + + + + Barclay S, Whyte R, Thiemann P, et al. An important but stressful part of their future work: medical students' attitudes to palliative care throughout their course. J Pain Symptom Manage 2015;49:231-42. + + + + + Why are newly qualified doctors unprepared to care for patients at the end of life? + + + J + Gibbins + + + + + R + Mccoubrie + + + + + K + Forbes + + + 10.1111/j.1365-2923.2010.03873.x + + + Med Educ + + 45 + + + + + Gibbins J, McCoubrie R, Forbes K. Why are newly qualified doctors unprepared to care for patients at the end of life? Med Educ 2011;45:389-99. + + + + + Palliative care in undergraduate medical education. status report and future directions + + + J + A + Billings + + + + + S + Block + + + + + JAMA + + 278 + + + + + Billings JA, Block S. Palliative care in undergraduate medical education. status report and future directions. JAMA 1997;278:733-8. + + + + + Medical student confidence in care of the dying and their family: a systematic review + + + G + Wells + + + + + E + Youssef + + + + + R + Winter + + + 10.1136/bmjspcare-2019-001977 + + + BMJ Support Palliat Care + + 2020 + + + + Wells G, Youssef E, Winter R, et al. Medical student confidence in care of the dying and their family: a systematic review. BMJ Support Palliat Care 2020:bmjspcare-2019-001977. + + + + + Medical education in the United States: do residents feel prepared + + + C + A + Chen + + + + + D + Kotliar + + + + + B + C + Drolet + + + 10.1007/s40037-015-0194-8 + + + Perspect Med Educ + + 4 + + + + + Chen CA, Kotliar D, Drolet BC. Medical education in the United States: do residents feel prepared? Perspect Med Educ 2015;4:181-5. + + + + + Confidence in palliative care issues by medical students and internal medicine residents + + + Acm + Storarri + + + + + G + D + De Castro + + + + + L + Castiglioni + + + 10.1136/bmjspcare-2017-001341 + + + BMJ Support Palliat Care + + 9 + e1 + + + + Storarri ACM, de Castro GD, Castiglioni L, et al. Confidence in palliative care issues by medical students and internal medicine residents. BMJ Support Palliat Care 2019;9:e1. + + + + + Knowledge and attitude of final -year medical students in Germany towards palliative care -an interinstitutional questionnaire-based study + + + M + Weber + + + + + S + Schmiedel + + + + + F + Nauck + + + 10.1186/1472-684X-10-19 + + + BMC Palliat Care + + 10 + 19 + + + + Weber M, Schmiedel S, Nauck F, et al. Knowledge and attitude of final -year medical students in Germany towards palliative care -an interinstitutional questionnaire-based study. BMC Palliat Care 2011;10:19. + + + + + Palliative care education in the undergraduate medical curricula: students' views on the importance of, their confidence in, and knowledge of palliative care + + + J + Pieters + + + + + Dhjm + Dolmans + + + + + Dml + Verstegen + + + 10.1186/s12904-019-0458-x + + + BMC Palliat Care + + 18 + 72 + + + + Pieters J, Dolmans DHJM, Verstegen DML, et al. Palliative care education in the undergraduate medical curricula: students' views on the importance of, their confidence in, and knowledge of palliative care. BMC Palliat Care 2019;18:72. + + + + + Design and evaluation of a learning assignment in the undergraduate medical curricula on the four dimensions of care: a mixed method study + + + J + Pieters + + + + + Dml + Verstegen + + + + + Dhjm + Dolmans + + + 10.1186/s12909-021-02681-0 + + + BMC Med Educ + + 21 + + + + + Pieters J, Verstegen DML, Dolmans DHJM, et al. Design and evaluation of a learning assignment in the undergraduate medical curricula on the four dimensions of care: a mixed method study. BMC Med Educ 2021;21:1-10. + + + + + End-of-life care in the Dutch medical curricula + + + J + De Bruin + + + + + M-J + Verhoef + + + + + Jpj + Slaets + + + 10.1007/s40037-018-0447-4 + + + Perspect Med Educ + + 7 + + + + + de Bruin J, Verhoef M-J, Slaets JPJ, et al. End-of-life care in the Dutch medical curricula. Perspect Med Educ 2018;7:325-31. + + + + + Education on end-of-life care in the medical curriculum: students' opinions and knowledge + + + Bam + Hesselink + + + + + Hrw + Pasman + + + + + G + Van Der Wal + + + 10.1089/jpm.2009.0291 + + + + J Palliat Med + + 13 + + + + + Downloaded from Original research + Hesselink BAM, Pasman HRW, van der Wal G, et al. Education on end-of-life care in the medical curriculum: students' opinions and knowledge. J Palliat Med 2010;13:381-7. copyright. on January 21, 2022 by guest. Protected by http://spcare.bmj.com/ BMJ Support Palliat Care: first published as 10.1136/bmjspcare-2021-003026 on 20 July 2021. Downloaded from Original research + + + + + Palliative care medical education in European universities: a descriptive study and numerical scoring system proposal for assessing educational development + + + J + M + Carrasco + + + + + T + J + Lynch + + + + + E + Garralda + + + 10.1016/j.jpainsymman.2015.04.019 + + + J Pain Symptom Manage + + 50 + + + + + Carrasco JM, Lynch TJ, Garralda E, et al. Palliative care medical education in European universities: a descriptive study and numerical scoring system proposal for assessing educational development. J Pain Symptom Manage 2015;50:516-23. + + + + + + </analytic> + <monogr> + <title level="j">Centra DNFvUM. Raamplan Artsopleiding + + + + + Centra DNFvUM. Raamplan Artsopleiding 2020;2020. + + + + + A national, palliative care competency framework for undergraduate medical curricula + + + J + Pieters + + + + + Dhjm + Dolmans + + + + + Mhj + Van Den Beuken-Van Everdingen + + + 10.3390/ijerph17072396 + + + Int J Environ Res Public Health + + 17 + 2396 + + + + Pieters J, Dolmans DHJM, van den Beuken-van Everdingen MHJ, et al. A national, palliative care competency framework for undergraduate medical curricula. Int J Environ Res Public Health 2020;17:2396. + + + + + 4C/ID in medical education: how to design an educational program based on whole-task learning: AMEE guide No + + + M + Vandewaetere + + + + + D + Manhaeve + + + + + B + Aertgeerts + + + 10.3109/0142159X.2014.928407 + + + Med Teach + + 37 + 93 + + + + + Vandewaetere M, Manhaeve D, Aertgeerts B, et al. 4C/ID in medical education: how to design an educational program based on whole-task learning: AMEE guide No. 93. Med Teach 2015;37:4-20. + + + + + Twelve tips for implementing whole-task curricula: how to make it work + + + Dhjm + Dolmans + + + + + Ihap + Wolfhagen + + + + + Jjg + Van Merriënboer + + + 10.3109/0142159X.2013.799640 + + + Med Teach + + 35 + + + + + Dolmans DHJM, Wolfhagen IHAP, Van Merriënboer JJG. Twelve tips for implementing whole-task curricula: how to make it work. Med Teach 2013;35:801-5. + + + + + Learning to care: medical students' reported value and evaluation of palliative care teaching involving meeting patients and reflective writing + + + E + Borgstrom + + + + + R + Morris + + + + + D + Wood + + + 10.1186/s12909-016-0827-6 + + + BMC Med Educ + + 16 + 306 + + + + Borgstrom E, Morris R, Wood D, et al. Learning to care: medical students' reported value and evaluation of palliative care teaching involving meeting patients and reflective writing. BMC Med Educ 2016;16:306. + + + + + Reflection and reflective practice in health professions education: a systematic review + + + K + Mann + + + + + J + Gordon + + + + + A + Macleod + + + 10.1007/s10459-007-9090-2 + + + Adv Health Sci Educ Theory Pract + + 14 + + + + + Mann K, Gordon J, MacLeod A. Reflection and reflective practice in health professions education: a systematic review. Adv Health Sci Educ Theory Pract 2009;14:595-621. + + + + + Words and wards: a model of reflective writing and its uses in medical education + + + J + Shapiro + + + + + D + Kasman + + + + + A + Shafer + + + 10.1007/s10912-006-9020-y + + + J Med Humanit + + 27 + + + + + Shapiro J, Kasman D, Shafer A. Words and wards: a model of reflective writing and its uses in medical education. J Med Humanit 2006;27:231-44. + + + + + The use of reflection in medical education: AMEE guide No. 44 + + + J + Sandars + + + + + S + John + + + 10.1080/01421590903050374 + + + Med Teach + + 31 + + + + + Sandars J, John S. The use of reflection in medical education: AMEE guide No. 44. Med Teach 2009;31:685-95. + + + + + The utility of template analysis in qualitative psychology research + + + J + Brooks + + + + + S + Mccluskey + + + + + E + Turley + + + 10.1080/14780887.2014.955224 + + + Qual Res Psychol + + 12 + + + + + Brooks J, McCluskey S, Turley E, et al. The utility of template analysis in qualitative psychology research. Qual Res Psychol 2015;12:202-22. + + + + + An evaluation of a support group for junior doctors working in palliative medicine + + + J + Feld + + + + + L + Heyse-Moore + + + 10.1177/1049909106290717 + + + Am J Hosp Palliat Care + + 23 + + + + + Feld J, Heyse-Moore L. An evaluation of a support group for junior doctors working in palliative medicine. Am J Hosp Palliat Care 2006;23:287-96. + + + + + Twelve tips to teach culturally sensitive palliative care + + + J + Suurmond + + + + + K + Lanting + + + + + X + De Voogd + + + 10.1080/0142159X.2020.1832650 + + + Med Teach + + 2020 + + + + Suurmond J, Lanting K, de Voogd X, et al. Twelve tips to teach culturally sensitive palliative care. Med Teach 2020:1-6. + + + + + Medical undergraduate palliative care education (UPCE) + + + D + Mcmahon + + + + + B + Wee + + + 10.1136/bmjspcare-2019-001965 + + + BMJ Support Palliat Care + + 11 + + + + + McMahon D, Wee B. Medical undergraduate palliative care education (UPCE). BMJ Support Palliat Care 2021;11:4-6. + + + + + Undergraduate training in palliative medicine: is more necessarily better + + + S + R + Mason + + + + + J + E + Ellershaw + + + 10.1177/0269216309351867 + + + Palliat Med + + 24 + + + + + Mason SR, Ellershaw JE. Undergraduate training in palliative medicine: is more necessarily better? Palliat Med 2010;24:306-9. + + + + + The effects of an early observational experience on medical students' attitudes toward end-of-life care + + + E + Wechter + + + + + D + C + O'gorman + + + + + M + K + Singh + + + 10.1177/1049909113505760 + + + Am J Hosp Palliat Care + + 32 + + + + + Wechter E, O'Gorman DC, Singh MK, et al. The effects of an early observational experience on medical students' attitudes toward end-of-life care. Am J Hosp Palliat Care 2015;32:52-60. + + + + + The status of medical education in end-of-life care: a national report + + + A + M + Sullivan + + + + + M + D + Lakoma + + + + + S + D + Block + + + 10.1046/j.1525-1497.2003.21215.x + + + J Gen Intern Med + + 18 + + + + + Sullivan AM, Lakoma MD, Block SD. The status of medical education in end-of-life care: a national report. J Gen Intern Med 2003;18:685-95. + + + + + Training in end-of-life care through interaction with dying patients + + + J + Binienda + + + + + K + Schwartz + + + + + D + Gaspar + + + 10.1097/00001888-200105000-00049 + + + Acad Med + + 76 + 517 + + + + Binienda J, Schwartz K, Gaspar D. Training in end-of-life care through interaction with dying patients. Acad Med 2001;76:517. + + + + + Junior doctors' views of how their undergraduate clinical electives in palliative care influenced their current practice of medicine + + + G + B + Crawford + + + + + S + C + Zambrano + + + 10.1097/ACM.0000000000000632 + + + + Acad Med + + 90 + + + + + Downloaded from + Crawford GB, Zambrano SC. Junior doctors' views of how their undergraduate clinical electives in palliative care influenced their current practice of medicine. Acad Med 2015;90:338-44. copyright. on January 21, 2022 by guest. Protected by http://spcare.bmj.com/ BMJ Support Palliat Care: first published as 10.1136/bmjspcare-2021-003026 on 20 July 2021. Downloaded from + + + +
+
+
+
\ No newline at end of file diff --git a/good_ai/tests/sus/data/bad.tei.xml b/good_ai/tests/sus/data/bad.tei.xml new file mode 100644 index 0000000..6b7df05 --- /dev/null +++ b/good_ai/tests/sus/data/bad.tei.xml @@ -0,0 +1,717 @@ + + + + + + BMJ + +

Copyright BMJ

+
+
+ + + + + + Ms + Jolien + Pieters + + j.pieters@maastrichtuniversity.nl + 0000-0002-9327-3977 + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education +
+ The Netherlands +
+
+
+ + + Daniëlle + Verstegen + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + Diana + Dolmans + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University + + + + + Evelien + Neis + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + + + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, School of Health Professions Education, Maastricht University, Maastricht, The Netherlands + + Department of Educational Development and Research + Faculty of Health, Medicine and Life Sciences + School of Health Professions Education + Maastricht University +
+ Maastricht + The Netherlands +
+
+
+ + + + + Centre of Expertise for Palliative Care, Maastricht UMC+, Maastricht, The Netherlands + + Centre of Expertise for Palliative Care + UMC+ +
+ Maastricht, Maastricht + The Netherlands +
+
+
+ + + Department of Educational Development and Research, Faculty of Health, Medicine and Life Sciences, Maastricht University, Maastricht 6229 ER, The Netherlands; + + + Spiritual dimension in palliative medicine: a qualitative study of learning tasks: medical students, teachers, educationalists +
+
+
+
+ + +
+ + + + + + + + + + + +
+ INTRODUCTION +

+ The need for palliative care is set to grow due to demographic changes, longer disease trajectories and higher comorbidity. + + Central to providing palliative care is the holistic, patient-centred and multidimensional approach, which addresses not only the physical, but also the psychological, social and spiritual dimension. + 1 + + + Providing palliative care is increasingly recognised as a universal responsibility of healthcare professionals + 2 3 + and all doctors will see patients with progressive life-limiting conditions at some point during their careers. + 4 + + Physicians, irrespective of specialism, should be both competent and confident in caring for the palliative care patient. + + Taking care of palliative care patients is typically associated with powerful and highly emotional situations affecting junior doctor's emotional well-being. + 5 + + + It is therefore important that Original research junior doctors develop the ability to guide palliative care patients during their medical training. + 6 + + + lthough there is a growing international movement to embed palliative care education in the undergraduate medical curricula, + 5 + this topic is not adequately addressed within all European medical universities. + + + Several studies demonstrate that medical students do not receive sufficient education in this area. + 7 8 + + + Students do not feel well prepared + [9] + [10] + [11] + [12] + and feel especially ill -prepared to raise and discuss the psychological, social and spiritual dimensions of care. + 13 + + + Their education primarily focuses on one dimension-the physicalwhile allowing the others to fall by the wayside. + 14 + + + tudents also report that self-care and reflection in the context of palliative care do not get much attention in their education. + 9 13 + + + In the Netherlands, the undergraduate medical education assigns only limited attention on palliative and end-of-life care. + 13 15-17 + + + This despite that the national competency framework states that the doctor should promote people's health and related quality of life, also in the palliative phase. + 18 + + + The competences that Dutch medical students need to acquire to provide good-quality palliative care have recently been set out in an educational framework. + 19 + + This framework specifies among others that the medical students should be able to talk to the patient and family about the incurable illness, prognosis and death, and discuss the four dimensions of care. + They should also be able to take care of their own well-being and reflect on their own spiritual needs, alongside their perceptions about life, death and dying. +

+

+ To bridge the gap between what students should learn and actually learn about spiritual care, we developed a coherent set of eight learning tasks. + Addressing the spiritual dimension is a complex task. + + According to current educational principles, learning complex tasks can be supported by providing authentic or realistic learning tasks + 20 + , by using principles of reflective learning, and should be integrated in the curricula. + + + Authentic tasks allow students to acquire knowledge, skills and attitudes in an integrated fashion, + 21 + which improves the transfer of the curriculum to the workplace. + 20 + + These authentic learning tasks can be interwoven in existing curricula in a horizontal and vertical integration manner. + + 3] + [24] + [25] + Through reflection, students are encouraged to think about their role as a physician, + 22 + foster professional growth, release the emotional burden of caring for palliative care patients and increase patient care skills. + 24 + + + Self-reflective training on the spiritual dimensions within the students' own lives is recommended. + 2 + + e developed a coherent set of realistic authentic learning tasks, in which students learn about and reflect on communication about the four dimensions of care, with a particular focus on the spiritual dimension. + The main aims of these learning tasks are that students learn about spiritual care, are able to talk about it with a palliative care patient, and to reflect on their spiritual experiences regarding life and death. + This article gives more insight into the usability and feasibility of these learning tasks from the stakeholders' perspectives, that is, medical students, teachers and educational scientists on the design of the learning tasks based on the educational principles of authentic educational scenarios, reflection and integration. + The research question is: How do medical students, teachers and educational scientists evaluate a set of coherent learning tasks focusing on the spiritual dimension of palliative care? +

+
+
+ METHODS +
+
+ Design +

+ Three groups of stakeholders were asked to participate in this evaluation: medical students, teachers, and educational scientists. + The students were interviewed in focus groups. + The teachers and educational scientists were questioned in individual interviews, due to their busy schedules. + This qualitative approach was used to gather in-depth information and insights from our stakeholders. +

+
+
+ Setting +

+ In the Netherlands, it takes 6 years to qualify as a physician. + In the first 3 years, the Bachelor's program, the student primarily acquires theory and medical knowledge. + In the last 3 years, the Master's program, the focus is on the application of knowledge in the work setting by letting students rotate between different internships. +

+
+
+ Coherent set of learning tasks +

+ + We designed a set of eight learning tasks (table + 1 + ; for a full description, see online supplemental appendix 1), designed to be integrated into the undergraduate medical curriculum. + + The designers included diversity and variations in teaching methods, diseases, treatment plans, age and gender of the patient. + The competencies to be acquired are described in box 1. + + These competences are a selection of the framework from Pieters et al. + 19 + The educational principles of authenticity, and reflection are incorporated into the set of learning tasks (see table + 2 + ). + +

+
+
+ Original research +
+
+ Participants +

+ Three groups of stakeholders were asked to participate in the evaluation: medical students, teachers and educational scientists. + The stakeholders came from faculties of medicine of four different universities in the Netherlands. +

+
+
+ Medical students (N=9) +

+ These stakeholders were asked to be interviewed in focus groups as they represented the educational users as learners. + Medical students in their final year of the Bachelor's program or studying for their Master's degree were invited. + These students have an informed opinion as to which tasks they deemed suitable for students and at what stage the tasks could best be implemented in the curriculum. +

+
+
+ Teachers of palliative care (N=9) +

+ These stakeholders were invited for their insight and experience in education and their substantive expertise in palliative care. + This group of stakeholders included medical specialists, mental healthcare providers and psychologists involved in teaching in undergraduate medical education. +

+

+ Educational scientists (N=4) +

+

+ These stakeholders were asked for their expertise in both educational design and the educational principles used in this learning program (Authentic learning, reflection and integration into existing courses). + The educational scientists worked at medical faculties. +

+
+
+ Instruments +

+ The teachers and educational scientists were interviewed individually, the students were interviewed in focus groups, using the same semistructured interview guide (see online supplemental appendix 2). + The interview guide asked for perceptions of the set of learning tasks, focusing on the educational learning principles that shaped them: authentic learning tasks, the principles of reflective learning, and the integration into the existing courses. +

+
+
+ Procedure +

+ Participants were recruited through purposeful sampling within the network of Pasemeco until data saturation was reached. + They were invited through an email that included the information letter. + Prior to their individual or focus group interview, the participants received a short explanatory video and a document with an overview of the set of learning tasks. + The interviews took no more than 45 min and were conducted by JP and EN. + The interviewees gave their written informed consent. +

+
+
+ Analysis +

+ The interviews were recorded and transcribed. + To support the qualitative data analysis, Atlas. + ti V.8 was used. + + The transcripts were analysed using template analysis, taking into account the step-by-step plan of Brooks et al. + 26 + Researchers JP, EN, and FW carried out the preliminary coding of the data, starting with determining the a priori themes. + + These themes were based on the components of the research question (authentic learning tasks; reflection and integration into the curriculum). + Two interview transcripts were read and coded independently by researchers JP, EN, and FW. + They discussed their findings, merged the themes into meaningful clusters, and provided an initial coding template. + This template was then applied independently by the three researchers to three other transcripts. +

+

+ After consultation, the template was finalised and applied to the full data set. + Themes were discussed and elaborated on iteratively with the whole research team. + JP and DV coded the other eight transcripts independently and discussed the results afterward. + The results were then discussed among all of the authors. +

+
+
+ Reflexivity +

+ This learning program was developed by a multidisciplinary group that included educational scientists (DV and DD), researchers (JP and EN) and physicians +

+
+
+ Box 1 : Intended competencies of the learning tasks +

+ Competencies: +

+

+ 1) The student communicates in a respectful and empathetic manner with patients and relatives. +

+

+ 2) The student makes the four dimensions (somatic, psychological, social and spiritual) of palliative care discussable with the patient and family. +

+

+ 3) The student reflects on distance and closeness in the treatment relationship with a palliative patient. + 4) The student reflects on his/her spiritual and existential views around life and death. +

+
+
+ RESULTS +
+
+ Authentic tasks +

+ The stakeholders agreed that the educational materials, including the case descriptions, the video clips and short movies, showed the students a realistic view of what they could encounter in the workplace. + The learning tasks were authentic and presented in a logical and structured order, from paper cases to a fullfledged conversation with a patient. + The learning tasks were authentic and presented in a logical and structured order, from paper cases to a fullfledged conversation with a patient. + The learning tasks were authentic and presented in a logical and structured order, from paper cases to a fullfledged conversation with a patient. + The stakeholders commented that the learning tasks and materials could be further improved if they provided a better reflection of the cultural diversity within the patient population. +

+

+ Students especially appreciated the clips in which real patients and doctors talked about palliative care and suggested that encouraging the possibility to discuss cases students have encountered in medical practice would further increase the authenticity. +

+
+
+ Reflective learning +

+ Stakeholders recognised reflection is a recurring element across the learning tasks. + They also consider it to be an important skill. + According to the stakeholders, the majority of the students should be able to reflect well on the various health dimensions, but the spiritual dimension can be emotionally charged. + Some stakeholders indicated that to be able to reflect on this properly, some life experiences would not go amiss. + Novice medical students' reflections lack the necessary depth in comparison with Master's degree students. + Both the teachers and students preferred oral group reflection. +

+

+ The students indicated that they attached great value to the interaction with their fellow students and that it helps them express themselves better. + Student safety must be ensured in order for them to properly reflect in a group. +

+
+
+ Reflective learning + Reflective learning + Reflective learning + Integration +

+ The stakeholders believed that the learning tasks should be integrated across the curriculum in different years. + However, it is important that this does not become too fragmented and that students can clearly see the cohesion. + Almost all stakeholders mentioned that the initial tasks (tasks 1-5) would be best implemented in the Bachelor's program, as the cases in these tasks revolve around the basis of palliative care. + The other learning tasks (tasks 6-8) were considered more appropriate for more advanced students during the internships as these center around communication with a simulated or real patient. + According to the stakeholders, they required experience with patients that students only acquire during the Master's degree. + There was no consensus as to when to start the learning trajectory. + Some stakeholders believed that palliative care education should start in the very first year, so that students become immediately aware that palliative care is a part of routine care. + But others thought it better to wait until the end of the Bachelor's program, because students would not be open to the subject or not fully understand the subject matter until then. + The implementation should also take into account the differences between students. + In the final learning task, students conduct an unsupervised interview with a palliative patient. + According to the stakeholders, the learning program does prepare students well for this interview, but it is still an emotionally charged task. + In order to protect both the patients and the students, the students must be well prepared and be able to take the lead in such an interview. + + Some students may need more training or support (table + 3 + ). + +

+
+
+ DISCUSSION +

+ The medical students, teachers, and educational scientists agree that the set of learning tasks was designed in line with contemporary instructional design guidelines. + Learning was clearly organised around authentic learning tasks relevant to the later profession, using paper, video cases, as well as simulations and real patients. + The tasks encourage the students to reflect on the four dimensions of palliative care and their personal values. + There were various options offered for the integration of the tasks into existing curricula. + The stakeholders also offered a number of suggestions to take into account, which will be discussed further. +

+

+ First, palliative care and the spiritual dimension in particular are emotionally charged subjects. + Extra attention to both student and patient safety is required. + Both the students and teachers attached great value to the group discussions and reflections, where a bond of trust and psychological safety between all of the attendees was considered to be essential. + + These findings are in line with the literature, where confidentially sharing experiences is needed and only possible in a group of people who know each other. + 2 27 + Second, the educational materials were considered authentic with variation in cases and learning methods, but to increase authenticity, the stakeholders suggested including more cultural diversity among the patient cases used in the learning tasks. + + Contemporary society is more culturally diverse than the current materials reflected. + + It is an important topic that should not be overlooked, given that many physicians are unfamiliar with the specific needs of ethnic minorities regarding palliative care and communication. + 28 + + hird, there was discussion on how to integrate learning tasks or palliative care education in general. + All participants agreed that this should start in the Bachelors and most agreed that it should be integrated vertically, throughout the undergraduate program and not as a stand-alone module. + Furthermore, it needs to be integrated horizontally, where relevant, for instance, in cardiology, oncology, humanities, and copyright. +

+

+ on January 21, 2022 by guest. + Protected by http://spcare.bmj.com/ + Original research communication training. + Integrating this set of learning tasks, therefore, mainly depends on the curriculum of a medical university and its identity as to where and when palliative care education can be implemented in the curriculum. + + The idea of vertically and horizontally integration is in line with recent educational research and guidelines. + 4 29 30 + + ome stakeholders argued that for learning about spiritual and palliative care in general, students should have some life experience and, therefore, it would be more suitable to start the program later in the bachelor. + Others argue that palliative care should be normalised for the students and should, therefore, start in the first year. + + The concern that early exposure may be emotionally challenging for young students, is the main reason why curricula integrate palliative care education in general later in the program. + 31 32 + + + However, research shows that direct experience with palliative care for first-year students is associated with positive effects on the students' attitudes regarding caring for palliative care patients. + 29 31 33 34 + + + Junior doctors who were trained earlier in palliative care have enhanced competencies of psychosocial and spiritual aspects of palliative care, communication, and self-awareness. + 34 + + + Thus, early exposure helps to normalise death and dying and the complex emotions that students and physicians can encounter while treating palliative care patients. + 31 33 34 + + nterestingly, in this study, students themselves had lively discussions on this topic too, but they mainly discussed more practical obstacles: when is there time for this in the curriculum? +

+

+ This study had some limitations. + To begin with, we evaluated the set of learning tasks prior to its implementation. + This meant that we could not reflect on the implementation process itself, leaving this to be researched at a later stage. + We also focused on the stakeholders in the educational setting: medical students, teachers, and educational scientists. + Patients 'I would also pay more attention to a more diversified background,(…). + The impression here is that it might be more about the classic white Dutch native than perhaps the Surinamese or Moroccan who gets into these problems and needs an essentially different approach' -I7 +

+

+ '(the clips, eds.)in the sense that the doctors who were talking, for example a [person] I have met in the clinic as well so that appeals more to the imagination. + That actually makes it even more authentic' -FG2 +

+

+ 'That there are not only videos but also something like a link to a wishlist, and a Volkskrant [newspaper] article so I think that's already very authentic. + You might want to make it a little more authentic by having students contribute cases they encounter' -FG2 Reflective learning 'I believe, indeed, that in real life that might be better than on paper, especially because you sometimes have to put something on paper and then you are not really sure or you feel differently or have not enough time and if you are really working in such a group, then ideas emerge or feelings that other people then evoke in you or you hear it and you think, oh that's right, I agree or I do not' -FG1 'What is also important here is that in your educational groups, or in the subgroup where you would discuss this, you have a bond of trust as well as feel safe' -I11 +

+
+
+ Integration +

+ And what might also be good timing,(…), is the second half of the third year of medicine. + Students are then very receptive to everything that is useful in their internships, so to speak, because then it is kind of a big thing that you will have to tackle soon and so you have to get started' -FG2 +

+

+ 'I see no reason not to do it (integration as of year 1, eds.). + And another advantage is that students will get an idea of -oh, yes, so this is part of the job as well? + It soon makes it more normal, because students often enrol with a strong idea of: we are going to make everyone better… but accepting that you can't make someone better will become the reality… but it is not really part of the idea of beginning students so, in that sense, I think: maybe it's also good to create some kind of awareness here' -I12 +

+

+ 'I believe you could use that simulation interview as a go/no-go for having an interview with a patient. + And, once again, they really are vulnerable patients, often in the final stage of life, who are often quite willing to contribute to the training, but who should also not be burdened disproportionately by oafs or students who are not interested or cut corners, and that such a patient is about to go home with a bad feeling after 45 minutes' -I8 FG, Focus Group; I, Interview. + were not involved in the evaluation, although they were involved in the overarching project within which the learning tasks were developed. + Last but not least, the educational principles within this learning program were tailored to the Dutch situation. + The educational principles and general setup of the design are internationally transferable, if tailored to the national situation. + Following the authenticity principle means that it is important to paint a realistic picture of the professional field. + If authenticity is to be guaranteed, learning tasks will have to be reviewed and adapted to be used in other countries. + This also regards to the principle of reflective learning. + The students involved in this study are already familiar with reflecting and sharing their opinion, since it plays a vital role in their curriculum. + On a national level there is a lively debate on end-of-life care, but not on the spiritual dimension. + Therefore, an emphasis was placed on the spiritual dimension. +

+

+ This study has several practical implications. + First, it pays off to integrate the program horizontally and vertically into the curriculum. + It depends on the specific tasks and the curriculum itself how and where the integration can best take place. + Some tasks will fit in well with education regarding communication or where clinical conditions are discussed such as oncology or lung disease. + To avoid fragmentation and guarantee that all aspects are covered, however, it is also important that someone has an overview of where and how palliative care is addressed in the curriculum, for example, a curriculum coordinator or someone specifically assigned with this task. + Furthermore, the tasks can always be adapted to specific contexts. + For example, it might be difficult to arrange interviews with palliative care patients. + Students could then interview a chronically ill patient instead of a palliative patient. + With explicit attention to communication and spiritual care education, it is possible to better prepare students for working in the professional field. + The spiritual dimension of care deserves explicit attention in the medical curriculum. +

+
+
+ + + +
+

+ , 2022 by guest. + Protected by http://spcare.bmj.com/ + Original research +

+
+
+
+
+ Table 1 + + +
+

+ The eight learning tasks +

+
+
+ + + + copyright. + + + 1 + Learning about the four dimensions: physical, psychological, social + + + + and emotional + + + 2 + Discussing and reflecting on palliative patient in the context of life + + + 3 + Reflection on personal views on life, death and dying + + + 4 + Recognising the spiritual dimension + + + 5 + Learning to integrate the spiritual dimension into counselling + + + 6 + To conduct an interview on meaning and purpose and work with + + + + the diamond model + + + 7 + Simulation interview with a palliative patient + + + 8 + Unsupervised interview with a palliative patient + +
+
+
+ Table 2 + + +
+

+ The incorporated educational principles in the different learning tasks +

+
+
+ + + + copyright. + + + + Authentic tasks Reflecting Integration + The materials provided are based on life-like, real cases encountered by professionals in their work. The learning tasks have been developed in close consultation with various care professionals. Furthermore, the student interviews a real patient they have met in the workplace. Reflection is a vital part of almost every learning task. Reflection is emphasised in several areas: ► Reflection on personal values, standards, and allocation of meaning and purpose (learning tasks 2, 3 and 5) ► Reflection on the influence of the four dimensions on the patient and the care trajectory (learning task 1, 4, 5 and 8) The learning tasks are developed for integration into existing courses. authentic tasks. The video clips used are based on + on January 21, 2022 by guest. Protected by http://spcare.bmj.com/ + +
+
+
+ Table 3 + + +
+

+ Themes and quotes of the stakeholders this(authenticity, eds.)in so far that patients tell you a lot of stories. + So, in that sense, they are all very authentic stories and this is how patients present themselves to doctors' -I10 'If you look at the learning tasks, you do see a clear structure. + From watching to an increasingly active role to eventually actually having a conversation with a patient, of course' -I8 +

+
+
+ + + Theme + Quotes + + + Authentic + 'I recognise + + + tasks + + +
+
+ BMJ Support Palliat Care: first published as 10.1136/bmjspcare-2021-003026 on 20 July 2021. Downloaded from + Pieters J, et al. BMJ Supportive & Palliative Care 2021;0:1-7. doi:10.1136/bmjspcare-2021-003026 + + + +
+
+ Acknowledgements +

+ We thank Judith Westen and Jimmy Frerejean for their support in designing these learning tasks. + We further thank the involved students, teachers and educational scientist. +

+
+
+ +
+
+

+ Contributors DV and FW developed the learning tasks; JP and EN conducted the data collection. + JP, EN and FW interpreted the data. + JP drafted the manuscript; DV, MvdBvE and DD. + supervised the study process. + All authors have read and agreed to the final version of the manuscript. +

+

+ Funding This work was supported by ZonMW (project number 80-84400-98-027). +

+
+
+ Competing interests None declared. +

+ Patient consent for publication Not required. +

+
+
+ Ethics approval Ethical clearance was obtained from the Netherlands Association for Medical Education Ethical Review +

+

+ Provenance and peer review Not commissioned; externally peer reviewed. +

+
+
+
+ Open access This is an open access article distributed in accordance with the Creative Commons Attribution Non +

+ Commercial (CC BY-NC 4.0) license, which permits others to distribute, remix, adapt, build upon this work noncommercially, and license their derivative works on different terms, provided the original work is properly cited, appropriate credit is given, any changes made indicated, and the use is noncommercial. + See: http:// creativecommons. + org/ licenses/ by-nc/ 4. 0/. +

+
+ +
+ +
+
+
\ No newline at end of file diff --git a/good_ai/tests/sus/data/parsed.py b/good_ai/tests/sus/data/parsed.py new file mode 100644 index 0000000..4826438 --- /dev/null +++ b/good_ai/tests/sus/data/parsed.py @@ -0,0 +1,2674 @@ +from sus.publication_tei.models import ( + Affiliation, + Author, + Meta, + Paragraph, + PublicationMetadata, + Text, + Title, +) + +metadata = PublicationMetadata( + language="en", + title="Spiritual dimension in palliative medicine: a qualitative study of learning tasks: medical students, teachers, educationalists", + publisher="BMJ", + doi="10.1136/bmjspcare-2021-003026", + md5="EAE447B92E88C8C72D4610586A269870", + publication_date="2021-07-20", + keywords=[], + reference_count=34, +) + +authors = [ + Author( + name="Ms Jolien Pieters", + orcid="0000-0002-9327-3977", + email="j.pieters@maastrichtuniversity.nl", + corresponding=True, + affiliations=[ + Affiliation( + institutions=["Maastricht University"], + departments=[ + "Department of Educational Development and Research", + "Faculty of Health, Medicine and Life Sciences", + "School of Health Professions Education", + ], + laboratories=[], + country="The Netherlands", + settlement="Maastricht", + ) + ], + coordinates="1,166.78,184.02,67.25,10.43", + ), + Author( + name="Daniëlle Verstegen", + orcid=None, + email=None, + corresponding=False, + affiliations=[ + Affiliation( + institutions=["Maastricht University"], + departments=[ + "Department of Educational Development and Research", + "Faculty of Health, Medicine and Life Sciences", + "School of Health Professions Education", + ], + laboratories=[], + country="The Netherlands", + settlement="Maastricht", + ) + ], + coordinates="1,261.76,184.02,94.40,10.43", + ), + Author( + name="Diana Dolmans", + orcid=None, + email=None, + corresponding=False, + affiliations=[ + Affiliation( + institutions=["Maastricht University"], + departments=[ + "Department of Educational Development and Research", + "Faculty of Health, Medicine and Life Sciences", + "School of Health Professions Education", + ], + laboratories=[], + country="The Netherlands", + settlement="Maastricht", + ) + ], + coordinates="1,368.73,184.02,74.73,10.43", + ), + Author( + name="Evelien Neis", + orcid=None, + email=None, + corresponding=False, + affiliations=[ + Affiliation( + institutions=["Maastricht University"], + departments=[ + "Department of Educational Development and Research", + "Faculty of Health, Medicine and Life Sciences", + "School of Health Professions Education", + ], + laboratories=[], + country="The Netherlands", + settlement="Maastricht", + ) + ], + coordinates="1,456.53,184.02,59.31,10.43", + ), + Author( + name="Franca Warmenhoven", + orcid=None, + email=None, + corresponding=False, + affiliations=[ + Affiliation( + institutions=["Maastricht University"], + departments=[ + "Department of Educational Development and Research", + "Faculty of Health, Medicine and Life Sciences", + "School of Health Professions Education", + ], + laboratories=[], + country="The Netherlands", + settlement="Maastricht", + ) + ], + coordinates="1,166.77,197.02,108.77,10.43", + ), + Author( + name="Marieke Van Den Beuken-Van Everdingen", + orcid=None, + email=None, + corresponding=False, + affiliations=[ + Affiliation( + institutions=[], + departments=["Centre of Expertise for Palliative Care"], + laboratories=["UMC+"], + country="The Netherlands", + settlement="Maastricht, Maastricht", + ) + ], + coordinates="1,289.21,197.02,210.89,10.43", + ), + Author( + name=None, + orcid=None, + email=None, + corresponding=False, + affiliations=[ + Affiliation( + institutions=["Maastricht University"], + departments=[ + "Department of Educational Development and Research", + "Faculty of Health, Medicine and Life Sciences", + ], + laboratories=[], + country="The Netherlands", + settlement="Maastricht", + ) + ], + coordinates=None, + ), +] + +content = [ + Meta(meta_type="abstract_start"), + Paragraph( + sentences=[ + Text( + content="Background Palliative care is gaining importance within the physician's range of duties.", + document_order=0, + coordinates="1,166.78,241.37,137.11,8.06;1,166.78,253.96,153.49,8.06;1,166.78,266.56,24.09,8.06", + ), + Text( + content="In the undergraduate medical curriculum, education on the four dimensions of care is insufficient.", + document_order=1, + coordinates="1,193.23,266.56,150.18,8.06;1,166.78,279.16,157.13,8.06;1,166.78,291.76,41.88,8.06", + ), + Text( + content="The spiritual dimension is hardly addressed.", + document_order=2, + coordinates="1,211.02,291.76,115.56,8.06;1,166.78,304.35,38.58,8.06", + ), + Text( + content="Therefore, we developed a coherent set of learning tasks targeted at learning to communicate about the spiritual dimension.", + document_order=3, + coordinates="1,207.72,304.35,130.84,8.06;1,166.78,316.95,156.63,8.06;1,166.78,329.55,159.63,8.06", + ), + Text( + content="The learning tasks are based on educational principles of authentic learning, reflective learning and longitudinal integration in the curriculum.", + document_order=4, + coordinates="1,166.78,342.14,158.70,8.06;1,166.78,354.74,149.23,8.06;1,166.78,367.34,156.03,8.06;1,166.78,379.93,40.60,8.06", + ), + Text( + content="This article reports on the feasibility of using these learning tasks in the medical curricula.", + document_order=5, + coordinates="1,209.74,379.93,127.81,8.06;1,166.78,392.53,156.48,8.06;1,166.78,405.13,33.05,8.06", + ), + Text( + content="Methods Teachers and educational scientists were interviewed and students were asked to evaluate the learning tasks in focus groups.", + document_order=6, + coordinates="1,166.78,417.73,165.28,8.06;1,166.78,430.32,164.53,8.06;1,166.78,442.92,156.81,8.06", + ), + Text( + content="Interview transcripts were analysed by three independent researchers.", + document_order=7, + coordinates="1,166.78,455.52,158.35,8.06;1,166.78,468.11,90.85,8.06", + ), + ] + ), + Title( + text=Text( + content="Results", + document_order=8, + coordinates="1,166.78,480.71,28.33,8.06", + ) + ), + Paragraph( + sentences=[ + Text( + content="The learning tasks encourage the students to reflect on the four dimensions of palliative care and their personal values.", + document_order=9, + coordinates="1,199.36,480.71,120.12,8.06;1,166.78,493.31,161.86,8.06;1,166.78,505.90,143.88,8.06", + ), + Text( + content="Learning was clearly organised around authentic learning tasks relevant to the later profession, using paper, video cases, as well as simulations and real patients.", + document_order=10, + coordinates="1,313.02,505.90,31.32,8.06;1,166.78,518.50,173.33,8.06;1,166.78,531.10,155.07,8.06;1,166.78,543.70,163.58,8.06;1,166.78,556.29,46.60,8.06", + ), + Text( + content="Participants suggest giving more attention to cultural diversity.", + document_order=11, + coordinates="1,215.74,556.29,116.97,8.06;1,166.78,568.89,105.46,8.06", + ), + Text( + content="As palliative care is an emotionally charged subject, the safety of both student and patient should be guaranteed.", + document_order=12, + coordinates="1,274.60,568.89,60.28,8.06;1,166.78,581.49,170.48,8.06;1,166.78,594.08,174.79,8.06", + ), + Text( + content="All participants indicated that the program should start in the bachelor phase and most agreed that it should be integrated vertically and horizontally throughout the undergraduate program, although there is some debate about the optimal moment to start.", + document_order=13, + coordinates="1,166.78,606.68,153.80,8.06;1,166.78,619.28,159.18,8.06;1,166.78,631.87,159.94,8.06;1,166.78,644.47,172.25,8.06;1,166.78,657.07,169.74,8.06;1,166.78,669.67,105.31,8.06", + ), + Text( + content="Conclusion The tasks, are authentic, encourage the students to reflect on the spiritual dimension of palliative care and are suitable for integration in the undergraduate medical curriculum.", + document_order=14, + coordinates="1,166.78,682.26,176.47,8.06;1,166.78,694.86,176.02,8.06;1,166.78,707.46,173.48,8.06;1,166.78,720.05,150.18,8.06", + ), + ] + ), + Title( + text=Text( + content="Key messages", + document_order=15, + coordinates="1,366.28,233.91,57.96,9.48", + ) + ), + Paragraph( + sentences=[ + Text( + content="What was already known?", + document_order=16, + coordinates="1,366.28,256.11,104.45,9.01", + ), + Text( + content="► Insufficient education on four dimensions of palliative care in undergraduate medical curriculum.", + document_order=17, + coordinates="1,366.27,267.11,160.84,9.23;1,378.28,278.11,154.38,9.01;1,378.28,289.11,40.11,9.01", + ), + Text( + content="► Especially the spiritual dimension is hardly addressed.", + document_order=18, + coordinates="1,366.27,300.11,164.09,9.23;1,378.28,311.11,38.66,9.01", + ), + ] + ), + Title( + text=Text( + content="What are the new findings?", + document_order=19, + coordinates="1,366.28,328.11,109.39,9.01", + ) + ), + Paragraph( + sentences=[ + Text( + content="► A coherent set of learning tasks developed in line with instructional design guidelines.", + document_order=20, + coordinates="1,366.27,339.11,165.32,9.23;1,378.28,350.11,153.86,9.01", + ), + Text( + content="► Stakeholder evaluations positive; confirm that these are authentic and encourage reflection.", + document_order=21, + coordinates="1,366.27,361.11,159.56,9.23;1,378.28,372.11,141.75,9.01;1,378.28,383.11,36.05,9.01", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="What is their significance?", + document_order=22, + coordinates="1,366.28,400.11,104.57,9.01", + ), + Text( + content="a. Clinical: Enhances palliative care education for medical students.", + document_order=23, + coordinates="1,366.28,411.11,166.50,9.01;1,378.28,422.11,75.08,9.01", + ), + Text( + content="b.", document_order=24, coordinates="1,366.28,433.11,6.50,9.01" + ), + Text( + content="Research: Suitable for integration in undergraduate medical curriculum.", + document_order=25, + coordinates="1,378.28,433.11,128.72,9.01;1,378.28,444.11,125.74,9.01", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="copyright.", + document_order=26, + coordinates="1,567.14,376.44,8.32,39.01", + ) + ] + ), + Meta(meta_type="abstract_end"), + Title( + text=Text( + content="INTRODUCTION", + document_order=27, + coordinates="1,360.28,486.04,71.51,9.24", + ) + ), + Paragraph( + sentences=[ + Text( + content="The need for palliative care is set to grow due to demographic changes, longer disease trajectories and higher comorbidity.", + document_order=28, + coordinates="1,360.28,498.01,178.54,9.36;1,360.28,510.17,178.57,9.36;1,360.28,522.33,145.45,9.36", + ), + Text( + content="Central to providing palliative care is the holistic, patient-centred and multidimensional approach, which addresses not only the physical, but also the psychological, social and spiritual dimension. 1", + document_order=29, + coordinates="1,507.95,522.33,30.90,9.36;1,360.28,534.49,178.57,9.36;1,360.28,546.65,178.56,9.36;1,360.28,558.81,178.60,9.36;1,360.28,570.97,178.57,9.36;1,360.28,583.13,104.24,9.36;1,464.27,580.99,3.69,6.21", + ), + Text( + content="Providing palliative care is increasingly recognised as a universal responsibility of healthcare professionals 2 3 and all doctors will see patients with progressive life-limiting conditions at some point during their careers. 4", + document_order=30, + coordinates="1,473.03,583.13,65.74,9.36;1,360.27,595.29,178.56,9.36;1,360.27,607.45,178.58,9.36;1,360.27,619.61,27.60,9.36;1,387.67,617.47,10.12,6.21;1,401.99,619.61,136.83,9.36;1,360.28,631.77,178.53,9.36;1,360.28,643.93,146.67,9.36;1,506.67,641.79,3.69,6.21", + ), + Text( + content="Physicians, irrespective of specialism, should be both competent and confident in caring for the palliative care patient.", + document_order=31, + coordinates="1,513.91,643.93,24.87,9.36;1,360.28,656.09,178.57,9.36;1,360.28,668.25,178.57,9.36;1,360.28,680.41,111.83,9.36", + ), + Text( + content="Taking care of palliative care patients is typically associated with powerful and highly emotional situations affecting junior doctor's emotional well-being. 5", + document_order=32, + coordinates="1,476.55,680.41,62.29,9.36;1,360.28,692.57,178.57,9.36;1,360.28,704.73,178.50,9.36;1,360.28,716.89,178.58,9.36;1,360.28,729.05,45.08,9.36;1,405.15,726.91,3.69,6.21", + ), + Text( + content="It is therefore important that Original research junior doctors develop the ability to guide palliative care patients during their medical training. 6", + document_order=33, + coordinates="1,413.24,729.05,125.59,9.36;2,60.50,24.52,85.87,9.48;2,56.50,45.11,232.01,9.36;2,56.50,57.11,155.00,9.36;2,211.23,54.96,3.69,6.21", + ), + Text( + content="lthough there is a growing international movement to embed palliative care education in the undergraduate medical curricula, 5 this topic is not adequately addressed within all European medical universities.", + document_order=34, + coordinates="2,65.50,69.11,222.87,9.36;2,56.50,81.11,231.89,9.36;2,56.50,93.11,101.07,9.36;2,157.56,90.96,3.69,6.21;2,165.88,93.11,122.50,9.36;2,56.50,105.11,231.88,9.36", + ), + Text( + content="Several studies demonstrate that medical students do not receive sufficient education in this area. 7 8", + document_order=35, + coordinates="2,56.50,117.11,231.88,9.36;2,56.50,129.11,184.10,9.36;2,240.59,126.96,8.94,6.21", + ), + Text( + content="Students do not feel well prepared [9] [10] [11] [12] and feel especially ill-prepared to raise and discuss the psychological, social and spiritual dimensions of care. 13", + document_order=36, + coordinates="2,251.89,129.11,36.49,9.36;2,56.50,141.11,117.08,9.36;2,173.58,138.96,14.37,6.21;2,192.97,141.11,95.43,9.36;2,56.50,153.11,231.90,9.36;2,56.50,165.11,148.73,9.36;2,205.23,162.96,7.37,6.21", + ), + Text( + content="Their education primarily focuses on one dimension-the physicalwhile allowing the others to fall by the wayside. 14", + document_order=37, + coordinates="2,217.75,165.11,70.64,9.36;2,56.50,177.11,231.90,9.36;2,56.50,189.11,224.52,9.36;2,281.01,186.96,7.37,6.21", + ), + Text( + content="tudents also report that self-care and reflection in the context of palliative care do not get much attention in their education. 9 13", + document_order=38, + coordinates="2,56.50,201.11,231.90,9.36;2,56.50,213.11,231.90,9.36;2,56.50,225.11,67.49,9.36;2,123.99,222.96,12.78,6.21", + ), + Text( + content="In the Netherlands, the undergraduate medical education assigns only limited attention on palliative and end-of-life care. 13 15-17", + document_order=39, + coordinates="2,139.37,225.11,149.01,9.36;2,56.50,237.11,231.88,9.36;2,56.50,249.11,146.92,9.36;2,203.41,246.96,28.09,6.21", + ), + Text( + content="This despite that the national competency framework states that the doctor should promote people's health and related quality of life, also in the palliative phase. 18", + document_order=40, + coordinates="2,235.51,249.11,52.87,9.36;2,56.50,261.11,231.88,9.36;2,56.50,273.11,231.90,9.36;2,56.50,285.11,172.10,9.36;2,228.59,282.96,7.37,6.21", + ), + Text( + content="The competences that Dutch medical students need to acquire to provide good-quality palliative care have recently been set out in an educational framework. 19", + document_order=41, + coordinates="2,237.86,285.11,50.53,9.36;2,56.50,297.11,231.89,9.36;2,56.50,309.11,231.89,9.36;2,56.50,321.11,168.01,9.36;2,224.50,318.96,7.37,6.21", + ), + Text( + content="This framework specifies among others that the medical students should be able to talk to the patient and family about the incurable illness, prognosis and death, and discuss the four dimensions of care.", + document_order=42, + coordinates="2,236.71,321.11,51.68,9.36;2,56.50,333.11,231.88,9.36;2,56.50,345.11,231.90,9.36;2,56.50,357.11,231.89,9.36;2,56.50,369.11,121.76,9.36", + ), + Text( + content="They should also be able to take care of their own well-being and reflect on their own spiritual needs, alongside their perceptions about life, death and dying.", + document_order=43, + coordinates="2,181.41,369.11,106.99,9.36;2,56.50,381.11,231.88,9.36;2,56.50,393.11,231.89,9.36;2,56.50,405.11,117.74,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="To bridge the gap between what students should learn and actually learn about spiritual care, we developed a coherent set of eight learning tasks.", + document_order=44, + coordinates="2,65.50,417.11,222.89,9.36;2,56.50,429.11,231.91,9.36;2,56.50,441.11,182.38,9.36", + ), + Text( + content="Addressing the spiritual dimension is a complex task.", + document_order=45, + coordinates="2,241.57,441.11,46.81,9.36;2,56.50,453.11,184.25,9.36", + ), + Text( + content="According to current educational principles, learning complex tasks can be supported by providing authentic or realistic learning tasks 20, by using principles of reflective learning, and should be integrated in the curricula.", + document_order=46, + coordinates="2,244.73,453.11,43.66,9.36;2,56.50,465.11,231.90,9.36;2,56.50,477.11,231.90,9.36;2,56.50,489.11,80.12,9.36;2,136.62,486.96,7.37,6.21;2,144.00,489.11,144.40,9.36;2,56.50,501.11,231.89,9.36", + ), + Text( + content="Authentic tasks allow students to acquire knowledge, skills and attitudes in an integrated fashion, 21 which improves the transfer of the curriculum to the workplace. 20", + document_order=47, + coordinates="2,56.50,513.11,231.89,9.36;2,56.50,525.11,194.45,9.36;2,250.94,522.96,7.37,6.21;2,262.50,525.11,25.89,9.36;2,56.50,537.11,231.90,9.36;2,56.50,549.11,24.87,9.36;2,81.36,546.96,7.37,6.21", + ), + Text( + content="These authentic learning tasks can be interwoven in existing curricula in a horizontal and vertical integration manner.", + document_order=48, + coordinates="2,93.25,549.11,195.14,9.36;2,56.50,561.11,231.90,9.36;2,56.50,573.11,84.74,9.36", + ), + Text( + content="3] [24] [25] Through reflection, students are encouraged to think about their role as a physician, 22 foster professional growth, release the emotional burden of caring for palliative care patients and increase patient care skills. 24", + document_order=49, + coordinates="2,91.86,594.96,7.22,6.21;2,101.14,597.11,187.26,9.36;2,56.50,609.11,165.28,9.36;2,221.77,606.96,7.37,6.21;2,231.56,609.11,56.83,9.36;2,56.50,621.11,231.89,9.36;2,56.50,633.11,231.89,9.36;2,56.50,645.11,23.83,9.36;2,80.32,642.96,7.37,6.21", + ), + Text( + content="Self-reflective training on the spiritual dimensions within the students' own lives is recommended. 2", + document_order=50, + coordinates="2,90.92,645.11,197.48,9.36;2,56.51,657.11,227.44,9.36;2,283.94,654.96,3.69,6.21", + ), + Text( + content="e developed a coherent set of realistic authentic learning tasks, in which students learn about and reflect on communication about the four dimensions of care, with a particular focus on the spiritual dimension.", + document_order=51, + coordinates="2,65.50,669.11,222.90,9.36;2,56.50,681.11,231.90,9.36;2,56.50,693.11,231.88,9.36;2,56.50,705.11,212.47,9.36", + ), + Text( + content="The main aims of these learning tasks are that students learn about spiritual care, are able to talk about it with a palliative care patient, and to reflect on their spiritual experiences regarding life and death.", + document_order=52, + coordinates="2,271.54,705.11,16.86,9.36;2,56.50,717.11,231.88,9.36;2,56.50,729.11,231.90,9.36;2,306.89,45.11,231.90,9.36;2,306.89,57.11,157.54,9.36", + ), + Text( + content="This article gives more insight into the usability and feasibility of these learning tasks from the stakeholders' perspectives, that is, medical students, teachers and educational scientists on the design of the learning tasks based on the educational principles of authentic educational scenarios, reflection and integration.", + document_order=53, + coordinates="2,467.16,57.11,71.62,9.36;2,306.89,69.11,231.90,9.36;2,306.89,81.11,231.88,9.36;2,306.89,93.11,231.89,9.36;2,306.89,105.11,231.89,9.36;2,306.89,117.11,231.90,9.36;2,306.89,129.11,115.41,9.36", + ), + Text( + content="The research question is: How do medical students, teachers and educational scientists evaluate a set of coherent learning tasks focusing on the spiritual dimension of palliative care?", + document_order=54, + coordinates="2,426.79,129.11,112.00,9.36;2,306.89,141.11,231.89,9.36;2,306.89,153.11,231.90,9.36;2,306.89,165.11,228.96,9.36", + ), + ] + ), + Title( + text=Text( + content="METHODS", + document_order=55, + coordinates="2,306.89,189.29,47.13,9.24", + ) + ), + Title( + text=Text( + content="Design", + document_order=56, + coordinates="2,306.89,200.84,26.58,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="Three groups of stakeholders were asked to participate in this evaluation: medical students, teachers, and educational scientists.", + document_order=57, + coordinates="2,306.89,211.61,231.88,9.36;2,306.89,223.61,231.90,9.36;2,306.89,235.61,93.84,9.36", + ), + Text( + content="The students were interviewed in focus groups.", + document_order=58, + coordinates="2,404.50,235.61,134.28,9.36;2,306.89,247.61,70.68,9.36", + ), + Text( + content="The teachers and educational scientists were questioned in individual interviews, due to their busy schedules.", + document_order=59, + coordinates="2,381.54,247.61,157.23,9.36;2,306.89,259.61,231.89,9.36;2,306.89,271.61,92.69,9.36", + ), + Text( + content="This qualitative approach was used to gather in-depth information and insights from our stakeholders.", + document_order=60, + coordinates="2,404.55,271.61,134.24,9.36;2,306.89,283.61,231.88,9.36;2,306.89,295.61,73.76,9.36", + ), + ] + ), + Title( + text=Text( + content="Setting", + document_order=61, + coordinates="2,306.89,319.34,27.97,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="In the Netherlands, it takes 6 years to qualify as a physician.", + document_order=62, + coordinates="2,306.89,330.11,231.88,9.36;2,306.89,342.11,20.24,9.36", + ), + Text( + content="In the first 3 years, the Bachelor's program, the student primarily acquires theory and medical knowledge.", + document_order=63, + coordinates="2,330.85,342.11,207.93,9.36;2,306.89,354.11,231.89,9.36;2,306.89,366.11,22.54,9.36", + ), + Text( + content="In the last 3 years, the Master's program, the focus is on the application of knowledge in the work setting by letting students rotate between different internships.", + document_order=64, + coordinates="2,334.14,366.11,204.63,9.36;2,306.89,378.11,231.90,9.36;2,306.89,390.11,231.88,9.36;2,306.89,402.11,49.79,9.36", + ), + ] + ), + Title( + text=Text( + content="Coherent set of learning tasks", + document_order=65, + coordinates="2,306.89,425.84,116.44,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="We designed a set of eight learning tasks (table 1; for a full description, see online supplemental appendix 1), designed to be integrated into the undergraduate medical curriculum.", + document_order=66, + coordinates="2,306.89,436.61,231.86,9.36;2,306.89,448.61,231.89,9.36;2,306.89,460.61,231.87,9.36;2,306.89,472.61,86.70,9.36", + ), + Text( + content="The designers included diversity and variations in teaching methods, diseases, treatment plans, age and gender of the patient.", + document_order=67, + coordinates="2,397.51,472.61,141.26,9.36;2,306.89,484.61,231.89,9.36;2,306.89,496.61,178.30,9.36", + ), + Text( + content="The competencies to be acquired are described in box 1.", + document_order=68, + coordinates="2,487.66,496.61,51.11,9.36;2,306.89,508.61,202.74,9.36", + ), + Text( + content="These competences are a selection of the framework from Pieters et al. 19 The educational principles of authenticity, and reflection are incorporated into the set of learning tasks (see table 2).", + document_order=69, + coordinates="2,513.58,508.61,25.19,9.36;2,306.89,520.61,231.89,9.36;2,306.89,532.61,52.49,9.36;2,359.38,530.46,7.37,6.21;2,315.89,544.61,222.90,9.36;2,306.89,556.61,231.90,9.36;2,306.89,568.61,77.61,9.36", + ), + ] + ), + Title( + text=Text( + content="Original research", + document_order=70, + coordinates="3,448.91,24.52,85.87,9.48", + ) + ), + Title( + text=Text( + content="Participants", + document_order=71, + coordinates="3,56.50,225.44,36.35,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="Three groups of stakeholders were asked to participate in the evaluation: medical students, teachers and educational scientists.", + document_order=72, + coordinates="3,56.50,236.21,231.88,9.36;3,56.50,248.21,231.90,9.36;3,56.50,260.21,95.79,9.36", + ), + Text( + content="The stakeholders came from faculties of medicine of four different universities in the Netherlands.", + document_order=73, + coordinates="3,158.01,260.21,130.37,9.36;3,56.50,272.21,231.88,9.36;3,56.50,284.21,71.47,9.36", + ), + ] + ), + Title( + text=Text( + content="Medical students (N=9)", + document_order=74, + coordinates="3,56.50,309.28,73.82,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="These stakeholders were asked to be interviewed in focus groups as they represented the educational users as learners.", + document_order=75, + coordinates="3,56.50,320.05,231.89,9.36;3,56.50,332.05,231.87,9.36;3,56.50,344.05,48.14,9.36", + ), + Text( + content="Medical students in their final year of the Bachelor's program or studying for their Master's degree were invited.", + document_order=76, + coordinates="3,107.86,344.05,180.53,9.36;3,56.50,356.05,231.88,9.36;3,56.50,368.05,87.16,9.36", + ), + Text( + content="These students have an informed opinion as to which tasks they deemed suitable for students and at what stage the tasks could best be implemented in the curriculum.", + document_order=77, + coordinates="3,146.62,368.05,141.78,9.36;3,56.50,380.05,231.87,9.36;3,56.50,392.05,231.90,9.36;3,56.50,404.05,135.74,9.36", + ), + ] + ), + Title( + text=Text( + content="Teachers of palliative care (N=9)", + document_order=78, + coordinates="3,56.50,429.12,101.30,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="These stakeholders were invited for their insight and experience in education and their substantive expertise in palliative care.", + document_order=79, + coordinates="3,56.50,439.89,231.87,9.36;3,56.50,451.89,231.88,9.36;3,56.50,463.89,73.68,9.36", + ), + Text( + content="This group of stakeholders included medical specialists, mental healthcare providers and psychologists involved in teaching in undergraduate medical education.", + document_order=80, + coordinates="3,133.20,463.89,155.20,9.36;3,56.50,475.89,231.88,9.36;3,56.50,487.89,231.89,9.36;3,56.50,499.89,80.65,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="Educational scientists (N=4)", + document_order=81, + coordinates="3,306.89,44.93,87.73,7.82", + ) + ] + ), + Paragraph( + sentences=[ + Text( + content="These stakeholders were asked for their expertise in both educational design and the educational principles used in this learning program (Authentic learning, reflection and integration into existing courses).", + document_order=82, + coordinates="3,306.89,55.70,231.90,9.36;3,306.89,67.70,231.90,9.36;3,306.89,79.70,231.89,9.36;3,306.89,91.70,211.09,9.36", + ), + Text( + content="The educational scientists worked at medical faculties.", + document_order=83, + coordinates="3,521.90,91.70,16.86,9.36;3,306.89,103.70,212.67,9.36", + ), + ] + ), + Title( + text=Text( + content="Instruments", + document_order=84, + coordinates="3,306.89,129.24,46.29,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="The teachers and educational scientists were interviewed individually, the students were interviewed in focus groups, using the same semistructured interview guide (see online supplemental appendix 2).", + document_order=85, + coordinates="3,306.89,140.01,231.89,9.36;3,306.89,152.01,231.88,9.36;3,306.89,164.01,231.90,9.36;3,306.89,176.01,187.04,9.36", + ), + Text( + content="The interview guide asked for perceptions of the set of learning tasks, focusing on the educational learning principles that shaped them: authentic learning tasks, the principles of reflective learning, and the integration into the existing courses.", + document_order=86, + coordinates="3,496.36,176.01,42.42,9.36;3,306.89,188.01,231.89,9.36;3,306.89,200.01,231.89,9.36;3,306.89,212.01,231.88,9.36;3,306.89,224.01,231.88,9.36;3,306.89,236.01,70.09,9.36", + ), + ] + ), + Title( + text=Text( + content="Procedure", + document_order=87, + coordinates="3,306.89,261.55,38.67,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="Participants were recruited through purposeful sampling within the network of Pasemeco until data saturation was reached.", + document_order=88, + coordinates="3,306.89,272.32,231.89,9.36;3,306.89,284.32,231.91,9.36;3,306.89,296.32,105.21,9.36", + ), + Text( + content="They were invited through an email that included the information letter.", + document_order=89, + coordinates="3,417.20,296.32,121.57,9.36;3,306.89,308.32,195.27,9.36", + ), + Text( + content="Prior to their individual or focus group interview, the participants received a short explanatory video and a document with an overview of the set of learning tasks.", + document_order=90, + coordinates="3,505.29,308.32,33.50,9.36;3,306.89,320.32,231.90,9.36;3,306.89,332.32,231.89,9.36;3,306.89,344.32,231.88,9.36", + ), + Text( + content="The interviews took no more than 45 min and were conducted by JP and EN.", + document_order=91, + coordinates="3,306.89,356.32,231.90,9.36;3,306.89,368.32,109.85,9.36", + ), + Text( + content="The interviewees gave their written informed consent.", + document_order=92, + coordinates="3,419.96,368.32,118.81,9.36;3,306.89,380.32,111.65,9.36", + ), + ] + ), + Title( + text=Text( + content="Analysis", + document_order=93, + coordinates="3,306.89,405.87,32.08,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="The interviews were recorded and transcribed.", + document_order=94, + coordinates="3,306.89,416.64,215.11,9.36", + ), + Text( + content="To support the qualitative data analysis, Atlas.", + document_order=95, + coordinates="3,527.88,416.64,10.90,9.36;3,306.89,428.64,188.59,9.36", + ), + Text( + content="ti V.8 was used.", + document_order=96, + coordinates="3,495.48,428.64,43.29,9.36;3,306.89,440.64,22.13,9.36", + ), + Text( + content="The transcripts were analysed using template analysis, taking into account the step-by-step plan of Brooks et al. 26 Researchers JP, EN, and FW carried out the preliminary coding of the data, starting with determining the a priori themes.", + document_order=97, + coordinates="3,334.24,440.64,204.54,9.36;3,306.89,452.64,231.88,9.36;3,306.88,464.64,57.78,9.36;3,364.67,462.49,7.37,6.21;3,376.65,464.64,162.13,9.36;3,306.89,476.64,231.90,9.36;3,306.89,488.64,143.68,9.36", + ), + Text( + content="These themes were based on the components of the research question (authentic learning tasks; reflection and integration into the curriculum).", + document_order=98, + coordinates="3,454.64,488.64,84.12,9.36;3,306.89,500.64,231.89,9.36;3,306.89,512.64,231.89,9.36;3,306.89,524.64,91.86,9.36", + ), + Text( + content="Two interview transcripts were read and coded independently by researchers JP, EN, and FW.", + document_order=99, + coordinates="3,402.84,524.64,135.94,9.36;3,306.89,536.64,231.88,9.36;3,306.89,548.64,37.37,9.36", + ), + Text( + content="They discussed their findings, merged the themes into meaningful clusters, and provided an initial coding template.", + document_order=100, + coordinates="3,349.37,548.64,189.39,9.36;3,306.89,560.64,231.90,9.36;3,306.89,572.64,98.00,9.36", + ), + Text( + content="This template was then applied independently by the three researchers to three other transcripts.", + document_order=101, + coordinates="3,407.19,572.64,131.59,9.36;3,306.89,584.64,231.89,9.36;3,306.89,596.64,47.56,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="After consultation, the template was finalised and applied to the full data set.", + document_order=102, + coordinates="3,315.89,608.64,222.88,9.36;3,306.89,620.64,124.20,9.36", + ), + Text( + content="Themes were discussed and elaborated on iteratively with the whole research team.", + document_order=103, + coordinates="3,435.75,620.64,103.02,9.36;3,306.89,632.64,231.90,9.36;3,306.89,644.64,23.64,9.36", + ), + Text( + content="JP and DV coded the other eight transcripts independently and discussed the results afterward.", + document_order=104, + coordinates="3,335.58,644.64,203.19,9.36;3,306.89,656.64,212.79,9.36", + ), + Text( + content="The results were then discussed among all of the authors.", + document_order=105, + coordinates="3,521.92,656.64,16.86,9.36;3,306.89,668.64,225.50,9.36", + ), + ] + ), + Title( + text=Text( + content="Reflexivity", + document_order=106, + coordinates="3,306.89,694.18,41.27,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="This learning program was developed by a multidisciplinary group that included educational scientists (DV and DD), researchers (JP and EN) and physicians", + document_order=107, + coordinates="3,306.89,704.95,231.89,9.36;3,306.89,716.95,231.89,9.36;3,306.89,728.95,231.90,9.36", + ) + ] + ), + Title( + text=Text( + content="Box 1: Intended competencies of the learning tasks", + document_order=108, + coordinates="3,62.50,50.89,200.31,9.48;3,62.50,62.39,21.66,9.48", + ) + ), + Paragraph( + sentences=[ + Text( + content="Competencies:", + document_order=109, + coordinates="3,62.50,84.59,58.25,9.01", + ) + ] + ), + Paragraph( + sentences=[ + Text( + content="1) The student communicates in a respectful and empathetic manner with patients and relatives.", + document_order=110, + coordinates="3,62.50,95.59,216.64,9.01;3,62.50,106.59,127.30,9.01", + ) + ] + ), + Paragraph( + sentences=[ + Text( + content="2) The student makes the four dimensions (somatic, psychological, social and spiritual) of palliative care discussable with the patient and family.", + document_order=111, + coordinates="3,62.50,117.60,184.98,9.01;3,62.50,128.60,184.29,9.01;3,62.50,139.60,143.12,9.01", + ) + ] + ), + Paragraph( + sentences=[ + Text( + content="3) The student reflects on distance and closeness in the treatment relationship with a palliative patient.", + document_order=112, + coordinates="3,62.50,150.60,197.81,9.01;3,62.50,161.60,170.37,9.01", + ), + Text( + content="4) The student reflects on his/her spiritual and existential views around life and death.", + document_order=113, + coordinates="3,62.50,172.60,203.60,9.01;3,62.50,183.60,102.51,9.01", + ), + ] + ), + Title( + text=Text( + content="RESULTS", + document_order=114, + coordinates="4,56.50,106.74,38.66,9.24", + ) + ), + Title( + text=Text( + content="Authentic tasks", + document_order=115, + coordinates="4,56.50,118.29,60.04,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="The stakeholders agreed that the educational materials, including the case descriptions, the video clips and short movies, showed the students a realistic view of what they could encounter in the workplace.", + document_order=116, + coordinates="4,56.50,129.06,231.90,9.36;4,56.50,141.06,231.90,9.36;4,56.50,153.06,231.88,9.36;4,56.50,165.06,231.90,9.36", + ), + Text( + content="The learning tasks were authentic and presented in a logical and structured order, from paper cases to a fullfledged conversation with a patient.", + document_order=117, + coordinates="4,56.50,177.06,231.89,9.36;4,56.50,189.06,231.90,9.36;4,56.50,201.06,155.31,9.36", + ), + Text( + content="The stakeholders commented that the learning tasks and materials could be further improved if they provided a better reflection of the cultural diversity within the patient population.", + document_order=118, + coordinates="4,215.03,201.06,73.37,9.36;4,56.50,213.06,231.90,9.36;4,56.50,225.06,231.89,9.36;4,56.50,237.06,231.90,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="Students especially appreciated the clips in which real patients and doctors talked about palliative care and suggested that encouraging the possibility to discuss cases students have encountered in medical practice would further increase the authenticity.", + document_order=119, + coordinates="4,56.50,249.06,231.90,9.36;4,56.50,261.06,231.89,9.36;4,56.50,273.06,231.89,9.36;4,56.50,285.06,231.90,9.36;4,56.50,297.06,168.17,9.36", + ) + ] + ), + Title( + text=Text( + content="Reflective learning", + document_order=120, + coordinates="4,56.50,322.24,72.59,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="Stakeholders recognised reflection is a recurring element across the learning tasks.", + document_order=121, + coordinates="4,56.50,333.01,231.88,9.36;4,56.50,345.01,141.35,9.36", + ), + Text( + content="They also consider it to be an important skill.", + document_order=122, + coordinates="4,200.31,345.01,88.06,9.36;4,56.50,357.01,100.91,9.36", + ), + Text( + content="According to the stakeholders, the majority of the students should be able to reflect well on the various health dimensions, but the spiritual dimension can be emotionally charged.", + document_order=123, + coordinates="4,159.63,357.01,128.75,9.36;4,56.50,369.01,231.90,9.36;4,56.50,381.01,231.90,9.36;4,56.50,393.01,174.42,9.36", + ), + Text( + content="Some stakeholders indicated that to be able to reflect on this properly, some life experiences would not go amiss.", + document_order=124, + coordinates="4,235.46,393.01,52.94,9.36;4,56.50,405.01,231.89,9.36;4,56.50,417.01,198.88,9.36", + ), + Text( + content="Novice medical students' reflections lack the necessary depth in comparison with Master's degree students.", + document_order=125, + coordinates="4,257.91,417.01,30.47,9.36;4,56.50,429.01,231.88,9.36;4,56.50,441.01,192.96,9.36", + ), + Text( + content="Both the teachers and students preferred oral group reflection.", + document_order=126, + coordinates="4,251.93,441.01,36.47,9.36;4,56.50,453.01,231.88,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="The students indicated that they attached great value to the interaction with their fellow students and that it helps them express themselves better.", + document_order=127, + coordinates="4,56.50,465.01,231.90,9.36;4,56.50,477.01,231.89,9.36;4,56.50,489.01,168.94,9.36", + ), + Text( + content="Student safety must be ensured in order for them to properly reflect in a group.", + document_order=128, + coordinates="4,228.24,489.01,60.14,9.36;4,56.50,501.01,231.88,9.36;4,56.50,513.01,46.77,9.36", + ), + ] + ), + Title( + text=Text( + content="Integration", + document_order=129, + coordinates="4,56.50,538.19,43.55,7.82", + ) + ), + Paragraph( + sentences=[ + Text( + content="The stakeholders believed that the learning tasks should be integrated across the curriculum in different years.", + document_order=130, + coordinates="4,56.50,548.96,231.88,9.36;4,56.50,560.96,231.88,9.36", + ), + Text( + content="However, it is important that this does not become too fragmented and that students can clearly see the cohesion.", + document_order=131, + coordinates="4,56.50,572.96,231.89,9.36;4,56.50,584.96,231.89,9.36;4,56.50,596.96,20.38,9.36", + ), + Text( + content="Almost all stakeholders mentioned that the initial tasks (tasks 1-5) would be best implemented in the Bachelor's program, as the cases in these tasks revolve around the basis of palliative care.", + document_order=132, + coordinates="4,79.30,596.96,209.08,9.36;4,56.50,608.96,231.91,9.36;4,56.50,620.96,231.89,9.36;4,56.50,632.96,148.00,9.36", + ), + Text( + content="The other learning tasks (tasks 6-8) were considered more appropriate for more advanced students during the internships as these center around communication with a simulated or real patient.", + document_order=133, + coordinates="4,207.59,632.96,80.81,9.36;4,56.50,644.96,231.89,9.36;4,56.50,656.96,231.89,9.36;4,56.50,668.96,231.88,9.36;4,56.50,680.96,92.17,9.36", + ), + Text( + content="According to the stakeholders, they required experience with patients that students only acquire during the Master's degree.", + document_order=134, + coordinates="4,153.08,680.96,135.29,9.36;4,56.50,692.96,231.88,9.36;4,56.50,704.96,181.77,9.36", + ), + Text( + content="There was no consensus as to when to start the learning trajectory.", + document_order=135, + coordinates="4,242.68,704.96,45.72,9.36;4,56.50,716.96,231.90,9.36;4,56.50,728.96,19.29,9.36", + ), + Text( + content="Some stakeholders believed that palliative care education should start in the very first year, so that students become immediately aware that palliative care is a part of routine care.", + document_order=136, + coordinates="4,80.35,728.96,208.04,9.36;4,306.89,45.11,231.89,9.36;4,306.89,57.11,231.90,9.36;4,306.89,69.11,133.09,9.36", + ), + Text( + content="But others thought it better to wait until the end of the Bachelor's program, because students would not be open to the subject or not fully understand the subject matter until then.", + document_order=137, + coordinates="4,444.37,69.11,94.41,9.36;4,306.89,81.11,231.89,9.36;4,306.89,93.11,231.89,9.36;4,306.89,105.11,213.52,9.36", + ), + Text( + content="The implementation should also take into account the differences between students.", + document_order=138, + coordinates="4,315.89,117.11,222.89,9.36;4,306.89,129.11,142.94,9.36", + ), + Text( + content="In the final learning task, students conduct an unsupervised interview with a palliative patient.", + document_order=139, + coordinates="4,452.97,129.11,85.81,9.36;4,306.89,141.11,231.89,9.36;4,306.89,153.11,81.89,9.36", + ), + Text( + content="According to the stakeholders, the learning program does prepare students well for this interview, but it is still an emotionally charged task.", + document_order=140, + coordinates="4,391.67,153.11,147.11,9.36;4,306.89,165.11,231.88,9.36;4,306.89,177.11,231.90,9.36", + ), + Text( + content="In order to protect both the patients and the students, the students must be well prepared and be able to take the lead in such an interview.", + document_order=141, + coordinates="4,306.89,189.11,231.89,9.36;4,306.89,201.11,231.90,9.36;4,306.89,213.11,124.16,9.36", + ), + Text( + content="Some students may need more training or support (table 3).", + document_order=142, + coordinates="4,433.77,213.11,105.03,9.36;4,306.89,225.11,147.84,9.36", + ), + ] + ), + Title( + text=Text( + content="DISCUSSION", + document_order=143, + coordinates="4,306.89,249.29,56.35,9.24", + ) + ), + Paragraph( + sentences=[ + Text( + content="The medical students, teachers, and educational scientists agree that the set of learning tasks was designed in line with contemporary instructional design guidelines.", + document_order=144, + coordinates="4,306.89,261.11,231.89,9.36;4,306.89,273.11,231.90,9.36;4,306.89,285.11,231.89,9.36;4,306.89,297.11,22.23,9.36", + ), + Text( + content="Learning was clearly organised around authentic learning tasks relevant to the later profession, using paper, video cases, as well as simulations and real patients.", + document_order=145, + coordinates="4,331.78,297.11,207.01,9.36;4,306.89,309.11,231.89,9.36;4,306.89,321.11,231.89,9.36;4,306.89,333.11,36.27,9.36", + ), + Text( + content="The tasks encourage the students to reflect on the four dimensions of palliative care and their personal values.", + document_order=146, + coordinates="4,347.41,333.11,191.36,9.36;4,306.89,345.11,231.88,9.36;4,306.89,357.11,69.73,9.36", + ), + Text( + content="There were various options offered for the integration of the tasks into existing curricula.", + document_order=147, + coordinates="4,381.04,357.11,157.74,9.36;4,306.89,369.11,231.87,9.36", + ), + Text( + content="The stakeholders also offered a number of suggestions to take into account, which will be discussed further.", + document_order=148, + coordinates="4,306.89,381.11,231.90,9.36;4,306.89,393.11,225.88,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="First, palliative care and the spiritual dimension in particular are emotionally charged subjects.", + document_order=149, + coordinates="4,315.89,405.11,222.88,9.36;4,306.89,417.11,204.25,9.36", + ), + Text( + content="Extra attention to both student and patient safety is required.", + document_order=150, + coordinates="4,515.34,417.11,23.44,9.36;4,306.89,429.11,231.88,9.36", + ), + Text( + content="Both the students and teachers attached great value to the group discussions and reflections, where a bond of trust and psychological safety between all of the attendees was considered to be essential.", + document_order=151, + coordinates="4,306.89,441.11,231.88,9.36;4,306.89,453.11,231.87,9.36;4,306.89,465.11,231.87,9.36;4,306.89,477.11,178.53,9.36", + ), + Text( + content="These findings are in line with the literature, where confidentially sharing experiences is needed and only possible in a group of people who know each other. 2 27 Second, the educational materials were considered authentic with variation in cases and learning methods, but to increase authenticity, the stakeholders suggested including more cultural diversity among the patient cases used in the learning tasks.", + document_order=152, + coordinates="4,489.22,477.11,49.57,9.36;4,306.89,489.11,231.90,9.36;4,306.89,501.11,231.90,9.36;4,306.89,513.11,166.83,9.36;4,473.72,510.96,12.90,6.21;4,315.89,525.11,222.88,9.36;4,306.89,537.11,231.89,9.36;4,306.89,549.11,231.89,9.36;4,306.89,561.11,231.89,9.36;4,306.89,573.11,134.15,9.36", + ), + Text( + content="Contemporary society is more culturally diverse than the current materials reflected.", + document_order=153, + coordinates="4,443.75,573.11,95.04,9.36;4,306.89,585.11,231.90,9.36;4,306.89,597.11,39.97,9.36", + ), + Text( + content="It is an important topic that should not be overlooked, given that many physicians are unfamiliar with the specific needs of ethnic minorities regarding palliative care and communication. 28", + document_order=154, + coordinates="4,350.66,597.11,188.12,9.36;4,306.89,609.11,231.90,9.36;4,306.89,621.11,231.88,9.36;4,306.89,633.11,150.06,9.36;4,456.93,630.96,7.37,6.21", + ), + Text( + content="hird, there was discussion on how to integrate learning tasks or palliative care education in general.", + document_order=155, + coordinates="4,315.89,645.11,222.89,9.36;4,306.89,657.11,231.89,9.36", + ), + Text( + content="All participants agreed that this should start in the Bachelors and most agreed that it should be integrated vertically, throughout the undergraduate program and not as a stand-alone module.", + document_order=156, + coordinates="4,306.89,669.11,231.89,9.36;4,306.89,681.11,231.90,9.36;4,306.89,693.11,231.87,9.36;4,306.89,705.11,156.93,9.36", + ), + Text( + content="Furthermore, it needs to be integrated horizontally, where relevant, for instance, in cardiology, oncology, humanities, and copyright.", + document_order=157, + coordinates="4,469.68,705.11,69.11,9.36;4,306.89,717.11,231.90,9.36;4,306.89,729.11,231.89,9.36;4,567.14,376.44,8.32,39.01", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="on January 21, 2022 by guest.", + document_order=158, + coordinates="4,576.14,582.02,8.33,10.01;4,576.14,594.53,8.33,32.01;4,576.14,629.05,8.32,12.51;4,576.14,644.06,8.33,20.02;4,576.14,666.58,8.33,9.50;4,576.14,678.58,8.33,24.52", + ), + Text( + content="Protected by http://spcare.bmj.com/", + document_order=159, + coordinates="4,576.14,705.60,8.33,38.52;4,576.14,746.62,8.33,9.50;4,576.14,490.99,8.33,88.53", + ), + Text( + content="Original research communication training.", + document_order=160, + coordinates="5,448.91,24.52,85.87,9.48;5,56.50,512.98,104.79,9.36", + ), + Text( + content="Integrating this set of learning tasks, therefore, mainly depends on the curriculum of a medical university and its identity as to where and when palliative care education can be implemented in the curriculum.", + document_order=161, + coordinates="5,163.19,512.98,125.21,9.36;5,56.50,524.98,231.88,9.36;5,56.50,536.98,231.90,9.36;5,56.50,548.98,231.90,9.36;5,56.50,560.98,65.80,9.36", + ), + Text( + content="The idea of vertically and horizontally integration is in line with recent educational research and guidelines. 4 29 30", + document_order=162, + coordinates="5,124.97,560.98,163.40,9.36;5,56.50,572.98,231.89,9.36;5,56.50,584.98,64.18,9.36;5,120.67,582.83,22.12,6.21", + ), + Text( + content="ome stakeholders argued that for learning about spiritual and palliative care in general, students should have some life experience and, therefore, it would be more suitable to start the program later in the bachelor.", + document_order=163, + coordinates="5,65.50,596.98,222.90,9.36;5,56.50,608.98,231.89,9.36;5,56.50,620.98,231.89,9.36;5,56.50,632.98,231.90,9.36", + ), + Text( + content="Others argue that palliative care should be normalised for the students and should, therefore, start in the first year.", + document_order=164, + coordinates="5,56.50,644.98,231.89,9.36;5,56.50,656.98,231.89,9.36;5,56.50,668.98,20.34,9.36", + ), + Text( + content="The concern that early exposure may be emotionally challenging for young students, is the main reason why curricula integrate palliative care education in general later in the program. 31 32", + document_order=165, + coordinates="5,79.00,668.98,209.39,9.36;5,56.50,680.98,231.89,9.36;5,56.50,692.98,231.89,9.36;5,56.50,704.98,128.64,9.36;5,185.13,702.83,17.54,6.21", + ), + Text( + content="However, research shows that direct experience with palliative care for first-year students is associated with positive effects on the students' attitudes regarding caring for palliative care patients. 29 31 33 34", + document_order=166, + coordinates="5,206.90,704.98,81.49,9.36;5,56.50,716.98,231.89,9.36;5,56.50,728.98,231.87,9.36;5,306.89,512.98,231.91,9.36;5,306.89,524.98,56.36,9.36;5,363.24,522.83,34.36,6.21", + ), + Text( + content="Junior doctors who were trained earlier in palliative care have enhanced competencies of psychosocial and spiritual aspects of palliative care, communication, and self-awareness. 34", + document_order=167, + coordinates="5,400.07,524.98,138.71,9.36;5,306.89,536.98,231.89,9.36;5,306.89,548.98,231.90,9.36;5,306.89,560.98,164.04,9.36;5,470.92,558.83,7.37,6.21", + ), + Text( + content="Thus, early exposure helps to normalise death and dying and the complex emotions that students and physicians can encounter while treating palliative care patients. 31 33 34", + document_order=168, + coordinates="5,486.00,560.98,52.77,9.36;5,306.88,572.98,231.89,9.36;5,306.88,584.98,231.89,9.36;5,306.88,596.98,206.08,9.36;5,512.96,594.83,25.81,6.21", + ), + Text( + content="nterestingly, in this study, students themselves had lively discussions on this topic too, but they mainly discussed more practical obstacles: when is there time for this in the curriculum?", + document_order=169, + coordinates="5,306.89,608.98,231.89,9.36;5,306.89,620.98,231.88,9.36;5,306.89,632.98,231.87,9.36;5,306.89,644.98,112.32,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="This study had some limitations.", + document_order=170, + coordinates="5,315.89,656.98,141.26,9.36", + ), + Text( + content="To begin with, we evaluated the set of learning tasks prior to its implementation.", + document_order=171, + coordinates="5,460.51,656.98,78.29,9.36;5,306.89,668.98,231.88,9.36;5,306.89,680.98,46.39,9.36", + ), + Text( + content="This meant that we could not reflect on the implementation process itself, leaving this to be researched at a later stage.", + document_order=172, + coordinates="5,357.66,680.98,181.12,9.36;5,306.89,692.98,231.89,9.36;5,306.89,704.98,137.26,9.36", + ), + Text( + content="We also focused on the stakeholders in the educational setting: medical students, teachers, and educational scientists.", + document_order=173, + coordinates="5,449.30,704.98,89.46,9.36;5,306.89,716.98,231.89,9.36;5,306.89,728.98,195.29,9.36", + ), + Text( + content="Patients'I would also pay more attention to a more diversified background,(…).", + document_order=174, + coordinates="5,505.64,728.98,33.14,9.36;5,108.50,156.07,221.01,8.06", + ), + Text( + content="The impression here is that it might be more about the classic white Dutch native than perhaps the Surinamese or Moroccan who gets into these problems and needs an essentially different approach'-I7", + document_order=175, + coordinates="5,330.77,156.07,189.69,8.06;5,108.50,165.57,422.62,8.06;5,108.50,175.07,12.26,8.06", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="'(the clips, eds.)in the sense that the doctors who were talking, for example a [person] I have met in the clinic as well so that appeals more to the imagination.", + document_order=176, + coordinates="5,108.50,203.57,426.67,8.06;5,108.50,213.08,59.04,8.06", + ), + Text( + content="That actually makes it even more authentic'-FG2", + document_order=177, + coordinates="5,168.80,213.08,153.12,8.06", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="'That there are not only videos but also something like a link to a wishlist, and a Volkskrant [newspaper] article so I think that's already very authentic.", + document_order=178, + coordinates="5,108.50,241.58,429.85,8.06;5,108.50,251.08,30.85,8.06", + ), + Text( + content="You might want to make it a little more authentic by having students contribute cases they encounter'-FG2 Reflective learning'I believe, indeed, that in real life that might be better than on paper, especially because you sometimes have to put something on paper and then you are not really sure or you feel differently or have not enough time and if you are really working in such a group, then ideas emerge or feelings that other people then evoke in you or you hear it and you think, oh that's right, I agree or I do not'-FG1'What is also important here is that in your educational groups, or in the subgroup where you would discuss this, you have a bond of trust as well as feel safe'-I11", + document_order=179, + coordinates="5,140.60,251.08,334.78,8.06;5,60.50,263.46,29.61,8.06;5,60.50,272.96,25.03,8.06;5,108.50,263.46,419.16,8.06;5,108.50,272.96,418.97,8.06;5,108.50,282.46,386.39,8.06;5,108.50,310.96,424.83,8.06;5,108.50,320.47,76.74,8.06", + ), + ] + ), + Title( + text=Text( + content="Integration", + document_order=180, + coordinates="5,60.50,332.84,33.70,8.06", + ) + ), + Paragraph( + sentences=[ + Text( + content="And what might also be good timing,(…), is the second half of the third year of medicine.", + document_order=181, + coordinates="5,108.50,332.84,277.17,8.06", + ), + Text( + content="Students are then very receptive to everything that is useful in their internships, so to speak, because then it is kind of a big thing that you will have to tackle soon and so you have to get started'-FG2", + document_order=182, + coordinates="5,387.39,332.84,141.06,8.06;5,108.50,342.35,429.36,8.06;5,108.50,351.85,44.36,8.06", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="'I see no reason not to do it (integration as of year 1, eds.).", + document_order=183, + coordinates="5,108.50,380.35,181.60,8.06", + ), + Text( + content="And another advantage is that students will get an idea of-oh, yes, so this is part of the job as well?", + document_order=184, + coordinates="5,291.36,380.35,238.27,8.06;5,108.50,389.85,71.45,8.06", + ), + Text( + content="It soon makes it more normal, because students often enrol with a strong idea of: we are going to make everyone better… but accepting that you can't make someone better will become the reality… but it is not really part of the idea of beginning students so, in that sense, I think: maybe it's also good to create some kind of awareness here'-I12", + document_order=185, + coordinates="5,181.99,389.85,349.34,8.06;5,108.50,399.35,409.56,8.06;5,108.50,408.86,305.78,8.06", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="'I believe you could use that simulation interview as a go/no-go for having an interview with a patient.", + document_order=186, + coordinates="5,108.50,437.36,316.08,8.06", + ), + Text( + content="And, once again, they really are vulnerable patients, often in the final stage of life, who are often quite willing to contribute to the training, but who should also not be burdened disproportionately by oafs or students who are not interested or cut corners, and that such a patient is about to go home with a bad feeling after 45 minutes'-I8 FG, Focus Group; I, Interview.", + document_order=187, + coordinates="5,425.84,437.36,97.24,8.06;5,108.50,446.86,414.15,8.06;5,108.50,456.36,425.00,8.06;5,108.50,465.87,102.09,8.06;5,60.50,478.19,90.18,8.06", + ), + Text( + content="were not involved in the evaluation, although they were involved in the overarching project within which the learning tasks were developed.", + document_order=188, + coordinates="6,56.50,45.11,231.87,9.36;6,56.50,57.12,231.87,9.36;6,56.50,69.13,150.15,9.36", + ), + Text( + content="Last but not least, the educational principles within this learning program were tailored to the Dutch situation.", + document_order=189, + coordinates="6,210.12,69.13,78.29,9.36;6,56.50,81.14,231.87,9.36;6,56.50,93.15,159.03,9.36", + ), + Text( + content="The educational principles and general setup of the design are internationally transferable, if tailored to the national situation.", + document_order=190, + coordinates="6,218.82,93.15,69.57,9.36;6,56.50,105.16,231.89,9.36;6,56.50,117.17,231.90,9.36;6,56.50,129.18,39.88,9.36", + ), + Text( + content="Following the authenticity principle means that it is important to paint a realistic picture of the professional field.", + document_order=191, + coordinates="6,100.44,129.18,187.95,9.36;6,56.50,141.19,231.90,9.36;6,56.50,153.20,77.26,9.36", + ), + Text( + content="If authenticity is to be guaranteed, learning tasks will have to be reviewed and adapted to be used in other countries.", + document_order=192, + coordinates="6,137.41,153.20,150.97,9.36;6,56.50,165.21,231.89,9.36;6,56.50,177.22,127.15,9.36", + ), + Text( + content="This also regards to the principle of reflective learning.", + document_order=193, + coordinates="6,186.79,177.22,101.60,9.36;6,56.50,189.23,133.47,9.36", + ), + Text( + content="The students involved in this study are already familiar with reflecting and sharing their opinion, since it plays a vital role in their curriculum.", + document_order=194, + coordinates="6,193.19,189.23,95.19,9.36;6,56.50,201.24,231.89,9.36;6,56.50,213.25,231.88,9.36;6,56.50,225.26,49.64,9.36", + ), + Text( + content="On a national level there is a lively debate on end-of-life care, but not on the spiritual dimension.", + document_order=195, + coordinates="6,108.94,225.26,179.46,9.36;6,56.50,237.27,231.89,9.36", + ), + Text( + content="Therefore, an emphasis was placed on the spiritual dimension.", + document_order=196, + coordinates="6,56.51,249.28,231.88,9.36;6,56.51,261.29,47.17,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="This study has several practical implications.", + document_order=197, + coordinates="6,65.51,273.30,196.78,9.36", + ), + Text( + content="First, it pays off to integrate the program horizontally and vertically into the curriculum.", + document_order=198, + coordinates="6,266.39,273.30,22.01,9.36;6,56.51,285.31,231.87,9.36;6,56.51,297.32,160.16,9.36", + ), + Text( + content="It depends on the specific tasks and the curriculum itself how and where the integration can best take place.", + document_order=199, + coordinates="6,222.91,297.32,65.48,9.36;6,56.51,309.33,231.90,9.36;6,56.51,321.34,181.09,9.36", + ), + Text( + content="Some tasks will fit in well with education regarding communication or where clinical conditions are discussed such as oncology or lung disease.", + document_order=200, + coordinates="6,240.93,321.34,47.47,9.36;6,56.51,333.35,231.90,9.36;6,56.51,345.36,231.87,9.36;6,56.51,357.37,109.08,9.36", + ), + Text( + content="To avoid fragmentation and guarantee that all aspects are covered, however, it is also important that someone has an overview of where and how palliative care is addressed in the curriculum, for example, a curriculum coordinator or someone specifically assigned with this task.", + document_order=201, + coordinates="6,168.62,357.37,119.77,9.36;6,56.51,369.38,231.90,9.36;6,56.51,381.39,231.88,9.36;6,56.51,393.40,231.89,9.36;6,56.51,405.41,231.88,9.36;6,56.51,417.42,153.03,9.36", + ), + Text( + content="Furthermore, the tasks can always be adapted to specific contexts.", + document_order=202, + coordinates="6,213.64,417.42,74.76,9.36;6,56.51,429.43,213.40,9.36", + ), + Text( + content="For example, it might be difficult to arrange interviews with palliative care patients.", + document_order=203, + coordinates="6,273.72,429.43,14.69,9.36;6,56.51,441.44,231.89,9.36;6,56.51,453.45,119.44,9.36", + ), + Text( + content="Students could then interview a chronically ill patient instead of a palliative patient.", + document_order=204, + coordinates="6,178.45,453.45,109.95,9.36;6,56.51,465.46,231.90,9.36;6,56.51,477.47,32.57,9.36", + ), + Text( + content="With explicit attention to communication and spiritual care education, it is possible to better prepare students for working in the professional field.", + document_order=205, + coordinates="6,91.84,477.47,196.56,9.36;6,56.51,489.48,231.88,9.36;6,56.51,501.49,209.89,9.36", + ), + Text( + content="The spiritual dimension of care deserves explicit attention in the medical curriculum.", + document_order=206, + coordinates="6,271.55,501.49,16.86,9.36;6,56.51,513.50,231.89,9.36;6,56.51,525.51,112.93,9.36", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content=", 2022 by guest.", + document_order=207, + coordinates="5,581.69,629.05,2.77,12.51;5,576.14,644.06,8.33,20.02;5,576.14,666.58,8.33,9.50;5,576.14,678.58,8.33,24.52", + ), + Text( + content="Protected by http://spcare.bmj.com/", + document_order=208, + coordinates="5,576.14,705.60,8.33,38.52;5,576.14,746.62,8.33,9.50;5,576.14,490.99,8.33,88.53", + ), + Text( + content="Original research", + document_order=209, + coordinates="6,60.50,24.52,85.87,9.48", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="The eight learning tasks", + document_order=210, + coordinates="2,348.73,604.84,81.98,9.01", + ) + ] + ), + Paragraph( + sentences=[ + Text( + content="The incorporated educational principles in the different learning tasks", + document_order=211, + coordinates="3,98.34,541.72,188.43,9.01;3,60.50,552.72,47.85,9.01", + ) + ] + ), + Paragraph( + sentences=[ + Text( + content="Themes and quotes of the stakeholders this(authenticity, eds.)in so far that patients tell you a lot of stories.", + document_order=212, + coordinates="5,98.34,50.36,134.78,9.01;5,145.80,80.06,206.18,8.06", + ), + Text( + content="So, in that sense, they are all very authentic stories and this is how patients present themselves to doctors'-I10'If you look at the learning tasks, you do see a clear structure.", + document_order=213, + coordinates="5,353.71,80.06,182.09,8.06;5,108.50,89.56,161.34,8.06;5,108.50,118.06,189.47,8.06", + ), + Text( + content="From watching to an increasingly active role to eventually actually having a conversation with a patient, of course'-I8", + document_order=214, + coordinates="5,299.70,118.06,231.07,8.06;5,108.50,127.57,130.78,8.06", + ), + ] + ), + Meta(meta_type="acknowledgements_start"), + Title( + text=Text( + content="Acknowledgements", + document_order=215, + coordinates="6,56.50,548.07,69.90,8.06", + ) + ), + Paragraph( + sentences=[ + Text( + content="We thank Judith Westen and Jimmy Frerejean for their support in designing these learning tasks.", + document_order=216, + coordinates="6,130.65,548.07,129.88,7.96;6,56.50,557.58,218.13,7.96", + ), + Text( + content="We further thank the involved students, teachers and educational scientist.", + document_order=217, + coordinates="6,276.96,557.58,11.43,7.96;6,56.50,567.09,222.78,7.96;6,56.50,576.60,31.41,7.96", + ), + ] + ), + Meta(meta_type="acknowledgements_end"), + Meta(meta_type="annex_start"), + Paragraph( + sentences=[ + Text( + content="Contributors DV and FW developed the learning tasks; JP and EN conducted the data collection.", + document_order=218, + coordinates="6,56.50,590.11,227.18,8.07;6,56.50,599.62,124.24,7.96", + ), + Text( + content="JP, EN and FW interpreted the data.", + document_order=219, + coordinates="6,183.11,599.62,97.60,7.96;6,56.50,609.13,31.66,7.96", + ), + Text( + content="JP drafted the manuscript; DV, MvdBvE and DD.", + document_order=220, + coordinates="6,90.53,609.13,179.27,7.96", + ), + Text( + content="supervised the study process.", + document_order=221, + coordinates="6,56.50,618.64,105.25,7.96", + ), + Text( + content="All authors have read and agreed to the final version of the manuscript.", + document_order=222, + coordinates="6,164.11,618.64,119.80,7.96;6,56.50,628.15,137.67,7.96", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="Funding This work was supported by ZonMW (project number 80-84400-98-027).", + document_order=223, + coordinates="6,56.50,641.66,231.68,8.07;6,56.50,651.17,70.40,7.96", + ) + ] + ), + Title( + text=Text( + content="Competing interests None declared.", + document_order=224, + coordinates="6,56.50,664.68,132.07,8.07", + ) + ), + Paragraph( + sentences=[ + Text( + content="Patient consent for publication Not required.", + document_order=225, + coordinates="6,56.50,678.18,164.07,8.07", + ) + ] + ), + Title( + text=Text( + content="Ethics approval Ethical clearance was obtained from the Netherlands Association for Medical Education Ethical Review", + document_order=226, + coordinates="6,56.50,691.69,204.20,8.07;6,56.50,701.20,229.14,7.96", + ) + ), + Paragraph( + sentences=[ + Text( + content="Board (NVMO-ERB file 2020.5.3).", + document_order=227, + coordinates="6,56.50,710.71,128.95,7.96", + ), + Text( + content="Written consent was obtained from all participants.", + document_order=228, + coordinates="6,187.82,710.71,73.58,7.96;6,56.50,720.23,110.46,7.96", + ), + Text( + content="The participants gave their permission to use anonymised quotes.", + document_order=229, + coordinates="6,169.32,720.23,97.51,7.96;6,56.50,729.74,137.92,7.96", + ), + ] + ), + Paragraph( + sentences=[ + Text( + content="Provenance and peer review Not commissioned; externally peer reviewed.", + document_order=230, + coordinates="6,306.88,45.02,215.24,8.07;6,306.88,54.66,53.42,7.96", + ) + ] + ), + Title( + text=Text( + content="Data availability statement Data are available on reasonable request.", + document_order=231, + coordinates="6,306.88,68.30,218.74,8.07;6,306.88,77.94,28.76,7.96", + ) + ), + Title( + text=Text( + content="Open access This is an open access article distributed in accordance with the Creative Commons Attribution Non", + document_order=232, + coordinates="6,306.88,91.08,203.26,8.07;6,306.88,100.22,207.66,7.96", + ) + ), + Paragraph( + sentences=[ + Text( + content="Commercial (CC BY-NC 4.0) license, which permits others to distribute, remix, adapt, build upon this work noncommercially, and license their derivative works on different terms, provided the original work is properly cited, appropriate credit is given, any changes made indicated, and the use is noncommercial.", + document_order=233, + coordinates="6,306.88,109.36,214.63,7.96;6,306.88,118.49,195.94,7.96;6,306.88,127.63,220.30,7.96;6,306.88,136.77,231.89,7.96;6,306.88,145.91,230.06,7.96;6,306.88,155.04,44.63,7.96", + ), + Text( + content="See: http:// creativecommons.", + document_order=234, + coordinates="6,353.87,155.04,105.68,7.96", + ), + Text( + content="org/ licenses/ by-nc/ 4. 0/.", + document_order=235, + coordinates="6,459.54,155.04,73.63,7.96;6,306.88,164.18,9.45,7.96", + ), + ] + ), + Title( + text=Text( + content="ORCID iD", + document_order=236, + coordinates="6,306.88,181.82,39.98,8.06", + ) + ), + Paragraph( + sentences=[ + Text( + content="Jolien Pieters http:// orcid.", + document_order=237, + coordinates="6,306.88,191.46,94.21,7.96", + ), + Text( + content="org/ 0000-0002-9327-3977", + document_order=238, + coordinates="6,401.10,191.46,98.73,7.96", + ), + ] + ), + Meta(meta_type="annex_end"), +] +sentences = [ + Text( + content="Background Palliative care is gaining importance within the physician's range of duties.", + document_order=0, + coordinates="1,166.78,241.37,137.11,8.06;1,166.78,253.96,153.49,8.06;1,166.78,266.56,24.09,8.06", + ), + Text( + content="In the undergraduate medical curriculum, education on the four dimensions of care is insufficient.", + document_order=1, + coordinates="1,193.23,266.56,150.18,8.06;1,166.78,279.16,157.13,8.06;1,166.78,291.76,41.88,8.06", + ), + Text( + content="The spiritual dimension is hardly addressed.", + document_order=2, + coordinates="1,211.02,291.76,115.56,8.06;1,166.78,304.35,38.58,8.06", + ), + Text( + content="Therefore, we developed a coherent set of learning tasks targeted at learning to communicate about the spiritual dimension.", + document_order=3, + coordinates="1,207.72,304.35,130.84,8.06;1,166.78,316.95,156.63,8.06;1,166.78,329.55,159.63,8.06", + ), + Text( + content="The learning tasks are based on educational principles of authentic learning, reflective learning and longitudinal integration in the curriculum.", + document_order=4, + coordinates="1,166.78,342.14,158.70,8.06;1,166.78,354.74,149.23,8.06;1,166.78,367.34,156.03,8.06;1,166.78,379.93,40.60,8.06", + ), + Text( + content="This article reports on the feasibility of using these learning tasks in the medical curricula.", + document_order=5, + coordinates="1,209.74,379.93,127.81,8.06;1,166.78,392.53,156.48,8.06;1,166.78,405.13,33.05,8.06", + ), + Text( + content="Methods Teachers and educational scientists were interviewed and students were asked to evaluate the learning tasks in focus groups.", + document_order=6, + coordinates="1,166.78,417.73,165.28,8.06;1,166.78,430.32,164.53,8.06;1,166.78,442.92,156.81,8.06", + ), + Text( + content="Interview transcripts were analysed by three independent researchers.", + document_order=7, + coordinates="1,166.78,455.52,158.35,8.06;1,166.78,468.11,90.85,8.06", + ), + Text( + content="The learning tasks encourage the students to reflect on the four dimensions of palliative care and their personal values.", + document_order=9, + coordinates="1,199.36,480.71,120.12,8.06;1,166.78,493.31,161.86,8.06;1,166.78,505.90,143.88,8.06", + ), + Text( + content="Learning was clearly organised around authentic learning tasks relevant to the later profession, using paper, video cases, as well as simulations and real patients.", + document_order=10, + coordinates="1,313.02,505.90,31.32,8.06;1,166.78,518.50,173.33,8.06;1,166.78,531.10,155.07,8.06;1,166.78,543.70,163.58,8.06;1,166.78,556.29,46.60,8.06", + ), + Text( + content="Participants suggest giving more attention to cultural diversity.", + document_order=11, + coordinates="1,215.74,556.29,116.97,8.06;1,166.78,568.89,105.46,8.06", + ), + Text( + content="As palliative care is an emotionally charged subject, the safety of both student and patient should be guaranteed.", + document_order=12, + coordinates="1,274.60,568.89,60.28,8.06;1,166.78,581.49,170.48,8.06;1,166.78,594.08,174.79,8.06", + ), + Text( + content="All participants indicated that the program should start in the bachelor phase and most agreed that it should be integrated vertically and horizontally throughout the undergraduate program, although there is some debate about the optimal moment to start.", + document_order=13, + coordinates="1,166.78,606.68,153.80,8.06;1,166.78,619.28,159.18,8.06;1,166.78,631.87,159.94,8.06;1,166.78,644.47,172.25,8.06;1,166.78,657.07,169.74,8.06;1,166.78,669.67,105.31,8.06", + ), + Text( + content="Conclusion The tasks, are authentic, encourage the students to reflect on the spiritual dimension of palliative care and are suitable for integration in the undergraduate medical curriculum.", + document_order=14, + coordinates="1,166.78,682.26,176.47,8.06;1,166.78,694.86,176.02,8.06;1,166.78,707.46,173.48,8.06;1,166.78,720.05,150.18,8.06", + ), + Text( + content="What was already known?", + document_order=16, + coordinates="1,366.28,256.11,104.45,9.01", + ), + Text( + content="► Insufficient education on four dimensions of palliative care in undergraduate medical curriculum.", + document_order=17, + coordinates="1,366.27,267.11,160.84,9.23;1,378.28,278.11,154.38,9.01;1,378.28,289.11,40.11,9.01", + ), + Text( + content="► Especially the spiritual dimension is hardly addressed.", + document_order=18, + coordinates="1,366.27,300.11,164.09,9.23;1,378.28,311.11,38.66,9.01", + ), + Text( + content="► A coherent set of learning tasks developed in line with instructional design guidelines.", + document_order=20, + coordinates="1,366.27,339.11,165.32,9.23;1,378.28,350.11,153.86,9.01", + ), + Text( + content="► Stakeholder evaluations positive; confirm that these are authentic and encourage reflection.", + document_order=21, + coordinates="1,366.27,361.11,159.56,9.23;1,378.28,372.11,141.75,9.01;1,378.28,383.11,36.05,9.01", + ), + Text( + content="What is their significance?", + document_order=22, + coordinates="1,366.28,400.11,104.57,9.01", + ), + Text( + content="a. Clinical: Enhances palliative care education for medical students.", + document_order=23, + coordinates="1,366.28,411.11,166.50,9.01;1,378.28,422.11,75.08,9.01", + ), + Text(content="b.", document_order=24, coordinates="1,366.28,433.11,6.50,9.01"), + Text( + content="Research: Suitable for integration in undergraduate medical curriculum.", + document_order=25, + coordinates="1,378.28,433.11,128.72,9.01;1,378.28,444.11,125.74,9.01", + ), + Text( + content="copyright.", + document_order=26, + coordinates="1,567.14,376.44,8.32,39.01", + ), + Text( + content="The need for palliative care is set to grow due to demographic changes, longer disease trajectories and higher comorbidity.", + document_order=28, + coordinates="1,360.28,498.01,178.54,9.36;1,360.28,510.17,178.57,9.36;1,360.28,522.33,145.45,9.36", + ), + Text( + content="Central to providing palliative care is the holistic, patient-centred and multidimensional approach, which addresses not only the physical, but also the psychological, social and spiritual dimension. 1", + document_order=29, + coordinates="1,507.95,522.33,30.90,9.36;1,360.28,534.49,178.57,9.36;1,360.28,546.65,178.56,9.36;1,360.28,558.81,178.60,9.36;1,360.28,570.97,178.57,9.36;1,360.28,583.13,104.24,9.36;1,464.27,580.99,3.69,6.21", + ), + Text( + content="Providing palliative care is increasingly recognised as a universal responsibility of healthcare professionals 2 3 and all doctors will see patients with progressive life-limiting conditions at some point during their careers. 4", + document_order=30, + coordinates="1,473.03,583.13,65.74,9.36;1,360.27,595.29,178.56,9.36;1,360.27,607.45,178.58,9.36;1,360.27,619.61,27.60,9.36;1,387.67,617.47,10.12,6.21;1,401.99,619.61,136.83,9.36;1,360.28,631.77,178.53,9.36;1,360.28,643.93,146.67,9.36;1,506.67,641.79,3.69,6.21", + ), + Text( + content="Physicians, irrespective of specialism, should be both competent and confident in caring for the palliative care patient.", + document_order=31, + coordinates="1,513.91,643.93,24.87,9.36;1,360.28,656.09,178.57,9.36;1,360.28,668.25,178.57,9.36;1,360.28,680.41,111.83,9.36", + ), + Text( + content="Taking care of palliative care patients is typically associated with powerful and highly emotional situations affecting junior doctor's emotional well-being. 5", + document_order=32, + coordinates="1,476.55,680.41,62.29,9.36;1,360.28,692.57,178.57,9.36;1,360.28,704.73,178.50,9.36;1,360.28,716.89,178.58,9.36;1,360.28,729.05,45.08,9.36;1,405.15,726.91,3.69,6.21", + ), + Text( + content="It is therefore important that Original research junior doctors develop the ability to guide palliative care patients during their medical training. 6", + document_order=33, + coordinates="1,413.24,729.05,125.59,9.36;2,60.50,24.52,85.87,9.48;2,56.50,45.11,232.01,9.36;2,56.50,57.11,155.00,9.36;2,211.23,54.96,3.69,6.21", + ), + Text( + content="lthough there is a growing international movement to embed palliative care education in the undergraduate medical curricula, 5 this topic is not adequately addressed within all European medical universities.", + document_order=34, + coordinates="2,65.50,69.11,222.87,9.36;2,56.50,81.11,231.89,9.36;2,56.50,93.11,101.07,9.36;2,157.56,90.96,3.69,6.21;2,165.88,93.11,122.50,9.36;2,56.50,105.11,231.88,9.36", + ), + Text( + content="Several studies demonstrate that medical students do not receive sufficient education in this area. 7 8", + document_order=35, + coordinates="2,56.50,117.11,231.88,9.36;2,56.50,129.11,184.10,9.36;2,240.59,126.96,8.94,6.21", + ), + Text( + content="Students do not feel well prepared [9] [10] [11] [12] and feel especially ill-prepared to raise and discuss the psychological, social and spiritual dimensions of care. 13", + document_order=36, + coordinates="2,251.89,129.11,36.49,9.36;2,56.50,141.11,117.08,9.36;2,173.58,138.96,14.37,6.21;2,192.97,141.11,95.43,9.36;2,56.50,153.11,231.90,9.36;2,56.50,165.11,148.73,9.36;2,205.23,162.96,7.37,6.21", + ), + Text( + content="Their education primarily focuses on one dimension-the physicalwhile allowing the others to fall by the wayside. 14", + document_order=37, + coordinates="2,217.75,165.11,70.64,9.36;2,56.50,177.11,231.90,9.36;2,56.50,189.11,224.52,9.36;2,281.01,186.96,7.37,6.21", + ), + Text( + content="tudents also report that self-care and reflection in the context of palliative care do not get much attention in their education. 9 13", + document_order=38, + coordinates="2,56.50,201.11,231.90,9.36;2,56.50,213.11,231.90,9.36;2,56.50,225.11,67.49,9.36;2,123.99,222.96,12.78,6.21", + ), + Text( + content="In the Netherlands, the undergraduate medical education assigns only limited attention on palliative and end-of-life care. 13 15-17", + document_order=39, + coordinates="2,139.37,225.11,149.01,9.36;2,56.50,237.11,231.88,9.36;2,56.50,249.11,146.92,9.36;2,203.41,246.96,28.09,6.21", + ), + Text( + content="This despite that the national competency framework states that the doctor should promote people's health and related quality of life, also in the palliative phase. 18", + document_order=40, + coordinates="2,235.51,249.11,52.87,9.36;2,56.50,261.11,231.88,9.36;2,56.50,273.11,231.90,9.36;2,56.50,285.11,172.10,9.36;2,228.59,282.96,7.37,6.21", + ), + Text( + content="The competences that Dutch medical students need to acquire to provide good-quality palliative care have recently been set out in an educational framework. 19", + document_order=41, + coordinates="2,237.86,285.11,50.53,9.36;2,56.50,297.11,231.89,9.36;2,56.50,309.11,231.89,9.36;2,56.50,321.11,168.01,9.36;2,224.50,318.96,7.37,6.21", + ), + Text( + content="This framework specifies among others that the medical students should be able to talk to the patient and family about the incurable illness, prognosis and death, and discuss the four dimensions of care.", + document_order=42, + coordinates="2,236.71,321.11,51.68,9.36;2,56.50,333.11,231.88,9.36;2,56.50,345.11,231.90,9.36;2,56.50,357.11,231.89,9.36;2,56.50,369.11,121.76,9.36", + ), + Text( + content="They should also be able to take care of their own well-being and reflect on their own spiritual needs, alongside their perceptions about life, death and dying.", + document_order=43, + coordinates="2,181.41,369.11,106.99,9.36;2,56.50,381.11,231.88,9.36;2,56.50,393.11,231.89,9.36;2,56.50,405.11,117.74,9.36", + ), + Text( + content="To bridge the gap between what students should learn and actually learn about spiritual care, we developed a coherent set of eight learning tasks.", + document_order=44, + coordinates="2,65.50,417.11,222.89,9.36;2,56.50,429.11,231.91,9.36;2,56.50,441.11,182.38,9.36", + ), + Text( + content="Addressing the spiritual dimension is a complex task.", + document_order=45, + coordinates="2,241.57,441.11,46.81,9.36;2,56.50,453.11,184.25,9.36", + ), + Text( + content="According to current educational principles, learning complex tasks can be supported by providing authentic or realistic learning tasks 20, by using principles of reflective learning, and should be integrated in the curricula.", + document_order=46, + coordinates="2,244.73,453.11,43.66,9.36;2,56.50,465.11,231.90,9.36;2,56.50,477.11,231.90,9.36;2,56.50,489.11,80.12,9.36;2,136.62,486.96,7.37,6.21;2,144.00,489.11,144.40,9.36;2,56.50,501.11,231.89,9.36", + ), + Text( + content="Authentic tasks allow students to acquire knowledge, skills and attitudes in an integrated fashion, 21 which improves the transfer of the curriculum to the workplace. 20", + document_order=47, + coordinates="2,56.50,513.11,231.89,9.36;2,56.50,525.11,194.45,9.36;2,250.94,522.96,7.37,6.21;2,262.50,525.11,25.89,9.36;2,56.50,537.11,231.90,9.36;2,56.50,549.11,24.87,9.36;2,81.36,546.96,7.37,6.21", + ), + Text( + content="These authentic learning tasks can be interwoven in existing curricula in a horizontal and vertical integration manner.", + document_order=48, + coordinates="2,93.25,549.11,195.14,9.36;2,56.50,561.11,231.90,9.36;2,56.50,573.11,84.74,9.36", + ), + Text( + content="3] [24] [25] Through reflection, students are encouraged to think about their role as a physician, 22 foster professional growth, release the emotional burden of caring for palliative care patients and increase patient care skills. 24", + document_order=49, + coordinates="2,91.86,594.96,7.22,6.21;2,101.14,597.11,187.26,9.36;2,56.50,609.11,165.28,9.36;2,221.77,606.96,7.37,6.21;2,231.56,609.11,56.83,9.36;2,56.50,621.11,231.89,9.36;2,56.50,633.11,231.89,9.36;2,56.50,645.11,23.83,9.36;2,80.32,642.96,7.37,6.21", + ), + Text( + content="Self-reflective training on the spiritual dimensions within the students' own lives is recommended. 2", + document_order=50, + coordinates="2,90.92,645.11,197.48,9.36;2,56.51,657.11,227.44,9.36;2,283.94,654.96,3.69,6.21", + ), + Text( + content="e developed a coherent set of realistic authentic learning tasks, in which students learn about and reflect on communication about the four dimensions of care, with a particular focus on the spiritual dimension.", + document_order=51, + coordinates="2,65.50,669.11,222.90,9.36;2,56.50,681.11,231.90,9.36;2,56.50,693.11,231.88,9.36;2,56.50,705.11,212.47,9.36", + ), + Text( + content="The main aims of these learning tasks are that students learn about spiritual care, are able to talk about it with a palliative care patient, and to reflect on their spiritual experiences regarding life and death.", + document_order=52, + coordinates="2,271.54,705.11,16.86,9.36;2,56.50,717.11,231.88,9.36;2,56.50,729.11,231.90,9.36;2,306.89,45.11,231.90,9.36;2,306.89,57.11,157.54,9.36", + ), + Text( + content="This article gives more insight into the usability and feasibility of these learning tasks from the stakeholders' perspectives, that is, medical students, teachers and educational scientists on the design of the learning tasks based on the educational principles of authentic educational scenarios, reflection and integration.", + document_order=53, + coordinates="2,467.16,57.11,71.62,9.36;2,306.89,69.11,231.90,9.36;2,306.89,81.11,231.88,9.36;2,306.89,93.11,231.89,9.36;2,306.89,105.11,231.89,9.36;2,306.89,117.11,231.90,9.36;2,306.89,129.11,115.41,9.36", + ), + Text( + content="The research question is: How do medical students, teachers and educational scientists evaluate a set of coherent learning tasks focusing on the spiritual dimension of palliative care?", + document_order=54, + coordinates="2,426.79,129.11,112.00,9.36;2,306.89,141.11,231.89,9.36;2,306.89,153.11,231.90,9.36;2,306.89,165.11,228.96,9.36", + ), + Text( + content="Three groups of stakeholders were asked to participate in this evaluation: medical students, teachers, and educational scientists.", + document_order=57, + coordinates="2,306.89,211.61,231.88,9.36;2,306.89,223.61,231.90,9.36;2,306.89,235.61,93.84,9.36", + ), + Text( + content="The students were interviewed in focus groups.", + document_order=58, + coordinates="2,404.50,235.61,134.28,9.36;2,306.89,247.61,70.68,9.36", + ), + Text( + content="The teachers and educational scientists were questioned in individual interviews, due to their busy schedules.", + document_order=59, + coordinates="2,381.54,247.61,157.23,9.36;2,306.89,259.61,231.89,9.36;2,306.89,271.61,92.69,9.36", + ), + Text( + content="This qualitative approach was used to gather in-depth information and insights from our stakeholders.", + document_order=60, + coordinates="2,404.55,271.61,134.24,9.36;2,306.89,283.61,231.88,9.36;2,306.89,295.61,73.76,9.36", + ), + Text( + content="In the Netherlands, it takes 6 years to qualify as a physician.", + document_order=62, + coordinates="2,306.89,330.11,231.88,9.36;2,306.89,342.11,20.24,9.36", + ), + Text( + content="In the first 3 years, the Bachelor's program, the student primarily acquires theory and medical knowledge.", + document_order=63, + coordinates="2,330.85,342.11,207.93,9.36;2,306.89,354.11,231.89,9.36;2,306.89,366.11,22.54,9.36", + ), + Text( + content="In the last 3 years, the Master's program, the focus is on the application of knowledge in the work setting by letting students rotate between different internships.", + document_order=64, + coordinates="2,334.14,366.11,204.63,9.36;2,306.89,378.11,231.90,9.36;2,306.89,390.11,231.88,9.36;2,306.89,402.11,49.79,9.36", + ), + Text( + content="We designed a set of eight learning tasks (table 1; for a full description, see online supplemental appendix 1), designed to be integrated into the undergraduate medical curriculum.", + document_order=66, + coordinates="2,306.89,436.61,231.86,9.36;2,306.89,448.61,231.89,9.36;2,306.89,460.61,231.87,9.36;2,306.89,472.61,86.70,9.36", + ), + Text( + content="The designers included diversity and variations in teaching methods, diseases, treatment plans, age and gender of the patient.", + document_order=67, + coordinates="2,397.51,472.61,141.26,9.36;2,306.89,484.61,231.89,9.36;2,306.89,496.61,178.30,9.36", + ), + Text( + content="The competencies to be acquired are described in box 1.", + document_order=68, + coordinates="2,487.66,496.61,51.11,9.36;2,306.89,508.61,202.74,9.36", + ), + Text( + content="These competences are a selection of the framework from Pieters et al. 19 The educational principles of authenticity, and reflection are incorporated into the set of learning tasks (see table 2).", + document_order=69, + coordinates="2,513.58,508.61,25.19,9.36;2,306.89,520.61,231.89,9.36;2,306.89,532.61,52.49,9.36;2,359.38,530.46,7.37,6.21;2,315.89,544.61,222.90,9.36;2,306.89,556.61,231.90,9.36;2,306.89,568.61,77.61,9.36", + ), + Text( + content="Three groups of stakeholders were asked to participate in the evaluation: medical students, teachers and educational scientists.", + document_order=72, + coordinates="3,56.50,236.21,231.88,9.36;3,56.50,248.21,231.90,9.36;3,56.50,260.21,95.79,9.36", + ), + Text( + content="The stakeholders came from faculties of medicine of four different universities in the Netherlands.", + document_order=73, + coordinates="3,158.01,260.21,130.37,9.36;3,56.50,272.21,231.88,9.36;3,56.50,284.21,71.47,9.36", + ), + Text( + content="These stakeholders were asked to be interviewed in focus groups as they represented the educational users as learners.", + document_order=75, + coordinates="3,56.50,320.05,231.89,9.36;3,56.50,332.05,231.87,9.36;3,56.50,344.05,48.14,9.36", + ), + Text( + content="Medical students in their final year of the Bachelor's program or studying for their Master's degree were invited.", + document_order=76, + coordinates="3,107.86,344.05,180.53,9.36;3,56.50,356.05,231.88,9.36;3,56.50,368.05,87.16,9.36", + ), + Text( + content="These students have an informed opinion as to which tasks they deemed suitable for students and at what stage the tasks could best be implemented in the curriculum.", + document_order=77, + coordinates="3,146.62,368.05,141.78,9.36;3,56.50,380.05,231.87,9.36;3,56.50,392.05,231.90,9.36;3,56.50,404.05,135.74,9.36", + ), + Text( + content="These stakeholders were invited for their insight and experience in education and their substantive expertise in palliative care.", + document_order=79, + coordinates="3,56.50,439.89,231.87,9.36;3,56.50,451.89,231.88,9.36;3,56.50,463.89,73.68,9.36", + ), + Text( + content="This group of stakeholders included medical specialists, mental healthcare providers and psychologists involved in teaching in undergraduate medical education.", + document_order=80, + coordinates="3,133.20,463.89,155.20,9.36;3,56.50,475.89,231.88,9.36;3,56.50,487.89,231.89,9.36;3,56.50,499.89,80.65,9.36", + ), + Text( + content="Educational scientists (N=4)", + document_order=81, + coordinates="3,306.89,44.93,87.73,7.82", + ), + Text( + content="These stakeholders were asked for their expertise in both educational design and the educational principles used in this learning program (Authentic learning, reflection and integration into existing courses).", + document_order=82, + coordinates="3,306.89,55.70,231.90,9.36;3,306.89,67.70,231.90,9.36;3,306.89,79.70,231.89,9.36;3,306.89,91.70,211.09,9.36", + ), + Text( + content="The educational scientists worked at medical faculties.", + document_order=83, + coordinates="3,521.90,91.70,16.86,9.36;3,306.89,103.70,212.67,9.36", + ), + Text( + content="The teachers and educational scientists were interviewed individually, the students were interviewed in focus groups, using the same semistructured interview guide (see online supplemental appendix 2).", + document_order=85, + coordinates="3,306.89,140.01,231.89,9.36;3,306.89,152.01,231.88,9.36;3,306.89,164.01,231.90,9.36;3,306.89,176.01,187.04,9.36", + ), + Text( + content="The interview guide asked for perceptions of the set of learning tasks, focusing on the educational learning principles that shaped them: authentic learning tasks, the principles of reflective learning, and the integration into the existing courses.", + document_order=86, + coordinates="3,496.36,176.01,42.42,9.36;3,306.89,188.01,231.89,9.36;3,306.89,200.01,231.89,9.36;3,306.89,212.01,231.88,9.36;3,306.89,224.01,231.88,9.36;3,306.89,236.01,70.09,9.36", + ), + Text( + content="Participants were recruited through purposeful sampling within the network of Pasemeco until data saturation was reached.", + document_order=88, + coordinates="3,306.89,272.32,231.89,9.36;3,306.89,284.32,231.91,9.36;3,306.89,296.32,105.21,9.36", + ), + Text( + content="They were invited through an email that included the information letter.", + document_order=89, + coordinates="3,417.20,296.32,121.57,9.36;3,306.89,308.32,195.27,9.36", + ), + Text( + content="Prior to their individual or focus group interview, the participants received a short explanatory video and a document with an overview of the set of learning tasks.", + document_order=90, + coordinates="3,505.29,308.32,33.50,9.36;3,306.89,320.32,231.90,9.36;3,306.89,332.32,231.89,9.36;3,306.89,344.32,231.88,9.36", + ), + Text( + content="The interviews took no more than 45 min and were conducted by JP and EN.", + document_order=91, + coordinates="3,306.89,356.32,231.90,9.36;3,306.89,368.32,109.85,9.36", + ), + Text( + content="The interviewees gave their written informed consent.", + document_order=92, + coordinates="3,419.96,368.32,118.81,9.36;3,306.89,380.32,111.65,9.36", + ), + Text( + content="The interviews were recorded and transcribed.", + document_order=94, + coordinates="3,306.89,416.64,215.11,9.36", + ), + Text( + content="To support the qualitative data analysis, Atlas.", + document_order=95, + coordinates="3,527.88,416.64,10.90,9.36;3,306.89,428.64,188.59,9.36", + ), + Text( + content="ti V.8 was used.", + document_order=96, + coordinates="3,495.48,428.64,43.29,9.36;3,306.89,440.64,22.13,9.36", + ), + Text( + content="The transcripts were analysed using template analysis, taking into account the step-by-step plan of Brooks et al. 26 Researchers JP, EN, and FW carried out the preliminary coding of the data, starting with determining the a priori themes.", + document_order=97, + coordinates="3,334.24,440.64,204.54,9.36;3,306.89,452.64,231.88,9.36;3,306.88,464.64,57.78,9.36;3,364.67,462.49,7.37,6.21;3,376.65,464.64,162.13,9.36;3,306.89,476.64,231.90,9.36;3,306.89,488.64,143.68,9.36", + ), + Text( + content="These themes were based on the components of the research question (authentic learning tasks; reflection and integration into the curriculum).", + document_order=98, + coordinates="3,454.64,488.64,84.12,9.36;3,306.89,500.64,231.89,9.36;3,306.89,512.64,231.89,9.36;3,306.89,524.64,91.86,9.36", + ), + Text( + content="Two interview transcripts were read and coded independently by researchers JP, EN, and FW.", + document_order=99, + coordinates="3,402.84,524.64,135.94,9.36;3,306.89,536.64,231.88,9.36;3,306.89,548.64,37.37,9.36", + ), + Text( + content="They discussed their findings, merged the themes into meaningful clusters, and provided an initial coding template.", + document_order=100, + coordinates="3,349.37,548.64,189.39,9.36;3,306.89,560.64,231.90,9.36;3,306.89,572.64,98.00,9.36", + ), + Text( + content="This template was then applied independently by the three researchers to three other transcripts.", + document_order=101, + coordinates="3,407.19,572.64,131.59,9.36;3,306.89,584.64,231.89,9.36;3,306.89,596.64,47.56,9.36", + ), + Text( + content="After consultation, the template was finalised and applied to the full data set.", + document_order=102, + coordinates="3,315.89,608.64,222.88,9.36;3,306.89,620.64,124.20,9.36", + ), + Text( + content="Themes were discussed and elaborated on iteratively with the whole research team.", + document_order=103, + coordinates="3,435.75,620.64,103.02,9.36;3,306.89,632.64,231.90,9.36;3,306.89,644.64,23.64,9.36", + ), + Text( + content="JP and DV coded the other eight transcripts independently and discussed the results afterward.", + document_order=104, + coordinates="3,335.58,644.64,203.19,9.36;3,306.89,656.64,212.79,9.36", + ), + Text( + content="The results were then discussed among all of the authors.", + document_order=105, + coordinates="3,521.92,656.64,16.86,9.36;3,306.89,668.64,225.50,9.36", + ), + Text( + content="This learning program was developed by a multidisciplinary group that included educational scientists (DV and DD), researchers (JP and EN) and physicians", + document_order=107, + coordinates="3,306.89,704.95,231.89,9.36;3,306.89,716.95,231.89,9.36;3,306.89,728.95,231.90,9.36", + ), + Text( + content="Competencies:", + document_order=109, + coordinates="3,62.50,84.59,58.25,9.01", + ), + Text( + content="1) The student communicates in a respectful and empathetic manner with patients and relatives.", + document_order=110, + coordinates="3,62.50,95.59,216.64,9.01;3,62.50,106.59,127.30,9.01", + ), + Text( + content="2) The student makes the four dimensions (somatic, psychological, social and spiritual) of palliative care discussable with the patient and family.", + document_order=111, + coordinates="3,62.50,117.60,184.98,9.01;3,62.50,128.60,184.29,9.01;3,62.50,139.60,143.12,9.01", + ), + Text( + content="3) The student reflects on distance and closeness in the treatment relationship with a palliative patient.", + document_order=112, + coordinates="3,62.50,150.60,197.81,9.01;3,62.50,161.60,170.37,9.01", + ), + Text( + content="4) The student reflects on his/her spiritual and existential views around life and death.", + document_order=113, + coordinates="3,62.50,172.60,203.60,9.01;3,62.50,183.60,102.51,9.01", + ), + Text( + content="The stakeholders agreed that the educational materials, including the case descriptions, the video clips and short movies, showed the students a realistic view of what they could encounter in the workplace.", + document_order=116, + coordinates="4,56.50,129.06,231.90,9.36;4,56.50,141.06,231.90,9.36;4,56.50,153.06,231.88,9.36;4,56.50,165.06,231.90,9.36", + ), + Text( + content="The learning tasks were authentic and presented in a logical and structured order, from paper cases to a fullfledged conversation with a patient.", + document_order=117, + coordinates="4,56.50,177.06,231.89,9.36;4,56.50,189.06,231.90,9.36;4,56.50,201.06,155.31,9.36", + ), + Text( + content="The stakeholders commented that the learning tasks and materials could be further improved if they provided a better reflection of the cultural diversity within the patient population.", + document_order=118, + coordinates="4,215.03,201.06,73.37,9.36;4,56.50,213.06,231.90,9.36;4,56.50,225.06,231.89,9.36;4,56.50,237.06,231.90,9.36", + ), + Text( + content="Students especially appreciated the clips in which real patients and doctors talked about palliative care and suggested that encouraging the possibility to discuss cases students have encountered in medical practice would further increase the authenticity.", + document_order=119, + coordinates="4,56.50,249.06,231.90,9.36;4,56.50,261.06,231.89,9.36;4,56.50,273.06,231.89,9.36;4,56.50,285.06,231.90,9.36;4,56.50,297.06,168.17,9.36", + ), + Text( + content="Stakeholders recognised reflection is a recurring element across the learning tasks.", + document_order=121, + coordinates="4,56.50,333.01,231.88,9.36;4,56.50,345.01,141.35,9.36", + ), + Text( + content="They also consider it to be an important skill.", + document_order=122, + coordinates="4,200.31,345.01,88.06,9.36;4,56.50,357.01,100.91,9.36", + ), + Text( + content="According to the stakeholders, the majority of the students should be able to reflect well on the various health dimensions, but the spiritual dimension can be emotionally charged.", + document_order=123, + coordinates="4,159.63,357.01,128.75,9.36;4,56.50,369.01,231.90,9.36;4,56.50,381.01,231.90,9.36;4,56.50,393.01,174.42,9.36", + ), + Text( + content="Some stakeholders indicated that to be able to reflect on this properly, some life experiences would not go amiss.", + document_order=124, + coordinates="4,235.46,393.01,52.94,9.36;4,56.50,405.01,231.89,9.36;4,56.50,417.01,198.88,9.36", + ), + Text( + content="Novice medical students' reflections lack the necessary depth in comparison with Master's degree students.", + document_order=125, + coordinates="4,257.91,417.01,30.47,9.36;4,56.50,429.01,231.88,9.36;4,56.50,441.01,192.96,9.36", + ), + Text( + content="Both the teachers and students preferred oral group reflection.", + document_order=126, + coordinates="4,251.93,441.01,36.47,9.36;4,56.50,453.01,231.88,9.36", + ), + Text( + content="The students indicated that they attached great value to the interaction with their fellow students and that it helps them express themselves better.", + document_order=127, + coordinates="4,56.50,465.01,231.90,9.36;4,56.50,477.01,231.89,9.36;4,56.50,489.01,168.94,9.36", + ), + Text( + content="Student safety must be ensured in order for them to properly reflect in a group.", + document_order=128, + coordinates="4,228.24,489.01,60.14,9.36;4,56.50,501.01,231.88,9.36;4,56.50,513.01,46.77,9.36", + ), + Text( + content="The stakeholders believed that the learning tasks should be integrated across the curriculum in different years.", + document_order=130, + coordinates="4,56.50,548.96,231.88,9.36;4,56.50,560.96,231.88,9.36", + ), + Text( + content="However, it is important that this does not become too fragmented and that students can clearly see the cohesion.", + document_order=131, + coordinates="4,56.50,572.96,231.89,9.36;4,56.50,584.96,231.89,9.36;4,56.50,596.96,20.38,9.36", + ), + Text( + content="Almost all stakeholders mentioned that the initial tasks (tasks 1-5) would be best implemented in the Bachelor's program, as the cases in these tasks revolve around the basis of palliative care.", + document_order=132, + coordinates="4,79.30,596.96,209.08,9.36;4,56.50,608.96,231.91,9.36;4,56.50,620.96,231.89,9.36;4,56.50,632.96,148.00,9.36", + ), + Text( + content="The other learning tasks (tasks 6-8) were considered more appropriate for more advanced students during the internships as these center around communication with a simulated or real patient.", + document_order=133, + coordinates="4,207.59,632.96,80.81,9.36;4,56.50,644.96,231.89,9.36;4,56.50,656.96,231.89,9.36;4,56.50,668.96,231.88,9.36;4,56.50,680.96,92.17,9.36", + ), + Text( + content="According to the stakeholders, they required experience with patients that students only acquire during the Master's degree.", + document_order=134, + coordinates="4,153.08,680.96,135.29,9.36;4,56.50,692.96,231.88,9.36;4,56.50,704.96,181.77,9.36", + ), + Text( + content="There was no consensus as to when to start the learning trajectory.", + document_order=135, + coordinates="4,242.68,704.96,45.72,9.36;4,56.50,716.96,231.90,9.36;4,56.50,728.96,19.29,9.36", + ), + Text( + content="Some stakeholders believed that palliative care education should start in the very first year, so that students become immediately aware that palliative care is a part of routine care.", + document_order=136, + coordinates="4,80.35,728.96,208.04,9.36;4,306.89,45.11,231.89,9.36;4,306.89,57.11,231.90,9.36;4,306.89,69.11,133.09,9.36", + ), + Text( + content="But others thought it better to wait until the end of the Bachelor's program, because students would not be open to the subject or not fully understand the subject matter until then.", + document_order=137, + coordinates="4,444.37,69.11,94.41,9.36;4,306.89,81.11,231.89,9.36;4,306.89,93.11,231.89,9.36;4,306.89,105.11,213.52,9.36", + ), + Text( + content="The implementation should also take into account the differences between students.", + document_order=138, + coordinates="4,315.89,117.11,222.89,9.36;4,306.89,129.11,142.94,9.36", + ), + Text( + content="In the final learning task, students conduct an unsupervised interview with a palliative patient.", + document_order=139, + coordinates="4,452.97,129.11,85.81,9.36;4,306.89,141.11,231.89,9.36;4,306.89,153.11,81.89,9.36", + ), + Text( + content="According to the stakeholders, the learning program does prepare students well for this interview, but it is still an emotionally charged task.", + document_order=140, + coordinates="4,391.67,153.11,147.11,9.36;4,306.89,165.11,231.88,9.36;4,306.89,177.11,231.90,9.36", + ), + Text( + content="In order to protect both the patients and the students, the students must be well prepared and be able to take the lead in such an interview.", + document_order=141, + coordinates="4,306.89,189.11,231.89,9.36;4,306.89,201.11,231.90,9.36;4,306.89,213.11,124.16,9.36", + ), + Text( + content="Some students may need more training or support (table 3).", + document_order=142, + coordinates="4,433.77,213.11,105.03,9.36;4,306.89,225.11,147.84,9.36", + ), + Text( + content="The medical students, teachers, and educational scientists agree that the set of learning tasks was designed in line with contemporary instructional design guidelines.", + document_order=144, + coordinates="4,306.89,261.11,231.89,9.36;4,306.89,273.11,231.90,9.36;4,306.89,285.11,231.89,9.36;4,306.89,297.11,22.23,9.36", + ), + Text( + content="Learning was clearly organised around authentic learning tasks relevant to the later profession, using paper, video cases, as well as simulations and real patients.", + document_order=145, + coordinates="4,331.78,297.11,207.01,9.36;4,306.89,309.11,231.89,9.36;4,306.89,321.11,231.89,9.36;4,306.89,333.11,36.27,9.36", + ), + Text( + content="The tasks encourage the students to reflect on the four dimensions of palliative care and their personal values.", + document_order=146, + coordinates="4,347.41,333.11,191.36,9.36;4,306.89,345.11,231.88,9.36;4,306.89,357.11,69.73,9.36", + ), + Text( + content="There were various options offered for the integration of the tasks into existing curricula.", + document_order=147, + coordinates="4,381.04,357.11,157.74,9.36;4,306.89,369.11,231.87,9.36", + ), + Text( + content="The stakeholders also offered a number of suggestions to take into account, which will be discussed further.", + document_order=148, + coordinates="4,306.89,381.11,231.90,9.36;4,306.89,393.11,225.88,9.36", + ), + Text( + content="First, palliative care and the spiritual dimension in particular are emotionally charged subjects.", + document_order=149, + coordinates="4,315.89,405.11,222.88,9.36;4,306.89,417.11,204.25,9.36", + ), + Text( + content="Extra attention to both student and patient safety is required.", + document_order=150, + coordinates="4,515.34,417.11,23.44,9.36;4,306.89,429.11,231.88,9.36", + ), + Text( + content="Both the students and teachers attached great value to the group discussions and reflections, where a bond of trust and psychological safety between all of the attendees was considered to be essential.", + document_order=151, + coordinates="4,306.89,441.11,231.88,9.36;4,306.89,453.11,231.87,9.36;4,306.89,465.11,231.87,9.36;4,306.89,477.11,178.53,9.36", + ), + Text( + content="These findings are in line with the literature, where confidentially sharing experiences is needed and only possible in a group of people who know each other. 2 27 Second, the educational materials were considered authentic with variation in cases and learning methods, but to increase authenticity, the stakeholders suggested including more cultural diversity among the patient cases used in the learning tasks.", + document_order=152, + coordinates="4,489.22,477.11,49.57,9.36;4,306.89,489.11,231.90,9.36;4,306.89,501.11,231.90,9.36;4,306.89,513.11,166.83,9.36;4,473.72,510.96,12.90,6.21;4,315.89,525.11,222.88,9.36;4,306.89,537.11,231.89,9.36;4,306.89,549.11,231.89,9.36;4,306.89,561.11,231.89,9.36;4,306.89,573.11,134.15,9.36", + ), + Text( + content="Contemporary society is more culturally diverse than the current materials reflected.", + document_order=153, + coordinates="4,443.75,573.11,95.04,9.36;4,306.89,585.11,231.90,9.36;4,306.89,597.11,39.97,9.36", + ), + Text( + content="It is an important topic that should not be overlooked, given that many physicians are unfamiliar with the specific needs of ethnic minorities regarding palliative care and communication. 28", + document_order=154, + coordinates="4,350.66,597.11,188.12,9.36;4,306.89,609.11,231.90,9.36;4,306.89,621.11,231.88,9.36;4,306.89,633.11,150.06,9.36;4,456.93,630.96,7.37,6.21", + ), + Text( + content="hird, there was discussion on how to integrate learning tasks or palliative care education in general.", + document_order=155, + coordinates="4,315.89,645.11,222.89,9.36;4,306.89,657.11,231.89,9.36", + ), + Text( + content="All participants agreed that this should start in the Bachelors and most agreed that it should be integrated vertically, throughout the undergraduate program and not as a stand-alone module.", + document_order=156, + coordinates="4,306.89,669.11,231.89,9.36;4,306.89,681.11,231.90,9.36;4,306.89,693.11,231.87,9.36;4,306.89,705.11,156.93,9.36", + ), + Text( + content="Furthermore, it needs to be integrated horizontally, where relevant, for instance, in cardiology, oncology, humanities, and copyright.", + document_order=157, + coordinates="4,469.68,705.11,69.11,9.36;4,306.89,717.11,231.90,9.36;4,306.89,729.11,231.89,9.36;4,567.14,376.44,8.32,39.01", + ), + Text( + content="on January 21, 2022 by guest.", + document_order=158, + coordinates="4,576.14,582.02,8.33,10.01;4,576.14,594.53,8.33,32.01;4,576.14,629.05,8.32,12.51;4,576.14,644.06,8.33,20.02;4,576.14,666.58,8.33,9.50;4,576.14,678.58,8.33,24.52", + ), + Text( + content="Protected by http://spcare.bmj.com/", + document_order=159, + coordinates="4,576.14,705.60,8.33,38.52;4,576.14,746.62,8.33,9.50;4,576.14,490.99,8.33,88.53", + ), + Text( + content="Original research communication training.", + document_order=160, + coordinates="5,448.91,24.52,85.87,9.48;5,56.50,512.98,104.79,9.36", + ), + Text( + content="Integrating this set of learning tasks, therefore, mainly depends on the curriculum of a medical university and its identity as to where and when palliative care education can be implemented in the curriculum.", + document_order=161, + coordinates="5,163.19,512.98,125.21,9.36;5,56.50,524.98,231.88,9.36;5,56.50,536.98,231.90,9.36;5,56.50,548.98,231.90,9.36;5,56.50,560.98,65.80,9.36", + ), + Text( + content="The idea of vertically and horizontally integration is in line with recent educational research and guidelines. 4 29 30", + document_order=162, + coordinates="5,124.97,560.98,163.40,9.36;5,56.50,572.98,231.89,9.36;5,56.50,584.98,64.18,9.36;5,120.67,582.83,22.12,6.21", + ), + Text( + content="ome stakeholders argued that for learning about spiritual and palliative care in general, students should have some life experience and, therefore, it would be more suitable to start the program later in the bachelor.", + document_order=163, + coordinates="5,65.50,596.98,222.90,9.36;5,56.50,608.98,231.89,9.36;5,56.50,620.98,231.89,9.36;5,56.50,632.98,231.90,9.36", + ), + Text( + content="Others argue that palliative care should be normalised for the students and should, therefore, start in the first year.", + document_order=164, + coordinates="5,56.50,644.98,231.89,9.36;5,56.50,656.98,231.89,9.36;5,56.50,668.98,20.34,9.36", + ), + Text( + content="The concern that early exposure may be emotionally challenging for young students, is the main reason why curricula integrate palliative care education in general later in the program. 31 32", + document_order=165, + coordinates="5,79.00,668.98,209.39,9.36;5,56.50,680.98,231.89,9.36;5,56.50,692.98,231.89,9.36;5,56.50,704.98,128.64,9.36;5,185.13,702.83,17.54,6.21", + ), + Text( + content="However, research shows that direct experience with palliative care for first-year students is associated with positive effects on the students' attitudes regarding caring for palliative care patients. 29 31 33 34", + document_order=166, + coordinates="5,206.90,704.98,81.49,9.36;5,56.50,716.98,231.89,9.36;5,56.50,728.98,231.87,9.36;5,306.89,512.98,231.91,9.36;5,306.89,524.98,56.36,9.36;5,363.24,522.83,34.36,6.21", + ), + Text( + content="Junior doctors who were trained earlier in palliative care have enhanced competencies of psychosocial and spiritual aspects of palliative care, communication, and self-awareness. 34", + document_order=167, + coordinates="5,400.07,524.98,138.71,9.36;5,306.89,536.98,231.89,9.36;5,306.89,548.98,231.90,9.36;5,306.89,560.98,164.04,9.36;5,470.92,558.83,7.37,6.21", + ), + Text( + content="Thus, early exposure helps to normalise death and dying and the complex emotions that students and physicians can encounter while treating palliative care patients. 31 33 34", + document_order=168, + coordinates="5,486.00,560.98,52.77,9.36;5,306.88,572.98,231.89,9.36;5,306.88,584.98,231.89,9.36;5,306.88,596.98,206.08,9.36;5,512.96,594.83,25.81,6.21", + ), + Text( + content="nterestingly, in this study, students themselves had lively discussions on this topic too, but they mainly discussed more practical obstacles: when is there time for this in the curriculum?", + document_order=169, + coordinates="5,306.89,608.98,231.89,9.36;5,306.89,620.98,231.88,9.36;5,306.89,632.98,231.87,9.36;5,306.89,644.98,112.32,9.36", + ), + Text( + content="This study had some limitations.", + document_order=170, + coordinates="5,315.89,656.98,141.26,9.36", + ), + Text( + content="To begin with, we evaluated the set of learning tasks prior to its implementation.", + document_order=171, + coordinates="5,460.51,656.98,78.29,9.36;5,306.89,668.98,231.88,9.36;5,306.89,680.98,46.39,9.36", + ), + Text( + content="This meant that we could not reflect on the implementation process itself, leaving this to be researched at a later stage.", + document_order=172, + coordinates="5,357.66,680.98,181.12,9.36;5,306.89,692.98,231.89,9.36;5,306.89,704.98,137.26,9.36", + ), + Text( + content="We also focused on the stakeholders in the educational setting: medical students, teachers, and educational scientists.", + document_order=173, + coordinates="5,449.30,704.98,89.46,9.36;5,306.89,716.98,231.89,9.36;5,306.89,728.98,195.29,9.36", + ), + Text( + content="Patients'I would also pay more attention to a more diversified background,(…).", + document_order=174, + coordinates="5,505.64,728.98,33.14,9.36;5,108.50,156.07,221.01,8.06", + ), + Text( + content="The impression here is that it might be more about the classic white Dutch native than perhaps the Surinamese or Moroccan who gets into these problems and needs an essentially different approach'-I7", + document_order=175, + coordinates="5,330.77,156.07,189.69,8.06;5,108.50,165.57,422.62,8.06;5,108.50,175.07,12.26,8.06", + ), + Text( + content="'(the clips, eds.)in the sense that the doctors who were talking, for example a [person] I have met in the clinic as well so that appeals more to the imagination.", + document_order=176, + coordinates="5,108.50,203.57,426.67,8.06;5,108.50,213.08,59.04,8.06", + ), + Text( + content="That actually makes it even more authentic'-FG2", + document_order=177, + coordinates="5,168.80,213.08,153.12,8.06", + ), + Text( + content="'That there are not only videos but also something like a link to a wishlist, and a Volkskrant [newspaper] article so I think that's already very authentic.", + document_order=178, + coordinates="5,108.50,241.58,429.85,8.06;5,108.50,251.08,30.85,8.06", + ), + Text( + content="You might want to make it a little more authentic by having students contribute cases they encounter'-FG2 Reflective learning'I believe, indeed, that in real life that might be better than on paper, especially because you sometimes have to put something on paper and then you are not really sure or you feel differently or have not enough time and if you are really working in such a group, then ideas emerge or feelings that other people then evoke in you or you hear it and you think, oh that's right, I agree or I do not'-FG1'What is also important here is that in your educational groups, or in the subgroup where you would discuss this, you have a bond of trust as well as feel safe'-I11", + document_order=179, + coordinates="5,140.60,251.08,334.78,8.06;5,60.50,263.46,29.61,8.06;5,60.50,272.96,25.03,8.06;5,108.50,263.46,419.16,8.06;5,108.50,272.96,418.97,8.06;5,108.50,282.46,386.39,8.06;5,108.50,310.96,424.83,8.06;5,108.50,320.47,76.74,8.06", + ), + Text( + content="And what might also be good timing,(…), is the second half of the third year of medicine.", + document_order=181, + coordinates="5,108.50,332.84,277.17,8.06", + ), + Text( + content="Students are then very receptive to everything that is useful in their internships, so to speak, because then it is kind of a big thing that you will have to tackle soon and so you have to get started'-FG2", + document_order=182, + coordinates="5,387.39,332.84,141.06,8.06;5,108.50,342.35,429.36,8.06;5,108.50,351.85,44.36,8.06", + ), + Text( + content="'I see no reason not to do it (integration as of year 1, eds.).", + document_order=183, + coordinates="5,108.50,380.35,181.60,8.06", + ), + Text( + content="And another advantage is that students will get an idea of-oh, yes, so this is part of the job as well?", + document_order=184, + coordinates="5,291.36,380.35,238.27,8.06;5,108.50,389.85,71.45,8.06", + ), + Text( + content="It soon makes it more normal, because students often enrol with a strong idea of: we are going to make everyone better… but accepting that you can't make someone better will become the reality… but it is not really part of the idea of beginning students so, in that sense, I think: maybe it's also good to create some kind of awareness here'-I12", + document_order=185, + coordinates="5,181.99,389.85,349.34,8.06;5,108.50,399.35,409.56,8.06;5,108.50,408.86,305.78,8.06", + ), + Text( + content="'I believe you could use that simulation interview as a go/no-go for having an interview with a patient.", + document_order=186, + coordinates="5,108.50,437.36,316.08,8.06", + ), + Text( + content="And, once again, they really are vulnerable patients, often in the final stage of life, who are often quite willing to contribute to the training, but who should also not be burdened disproportionately by oafs or students who are not interested or cut corners, and that such a patient is about to go home with a bad feeling after 45 minutes'-I8 FG, Focus Group; I, Interview.", + document_order=187, + coordinates="5,425.84,437.36,97.24,8.06;5,108.50,446.86,414.15,8.06;5,108.50,456.36,425.00,8.06;5,108.50,465.87,102.09,8.06;5,60.50,478.19,90.18,8.06", + ), + Text( + content="were not involved in the evaluation, although they were involved in the overarching project within which the learning tasks were developed.", + document_order=188, + coordinates="6,56.50,45.11,231.87,9.36;6,56.50,57.12,231.87,9.36;6,56.50,69.13,150.15,9.36", + ), + Text( + content="Last but not least, the educational principles within this learning program were tailored to the Dutch situation.", + document_order=189, + coordinates="6,210.12,69.13,78.29,9.36;6,56.50,81.14,231.87,9.36;6,56.50,93.15,159.03,9.36", + ), + Text( + content="The educational principles and general setup of the design are internationally transferable, if tailored to the national situation.", + document_order=190, + coordinates="6,218.82,93.15,69.57,9.36;6,56.50,105.16,231.89,9.36;6,56.50,117.17,231.90,9.36;6,56.50,129.18,39.88,9.36", + ), + Text( + content="Following the authenticity principle means that it is important to paint a realistic picture of the professional field.", + document_order=191, + coordinates="6,100.44,129.18,187.95,9.36;6,56.50,141.19,231.90,9.36;6,56.50,153.20,77.26,9.36", + ), + Text( + content="If authenticity is to be guaranteed, learning tasks will have to be reviewed and adapted to be used in other countries.", + document_order=192, + coordinates="6,137.41,153.20,150.97,9.36;6,56.50,165.21,231.89,9.36;6,56.50,177.22,127.15,9.36", + ), + Text( + content="This also regards to the principle of reflective learning.", + document_order=193, + coordinates="6,186.79,177.22,101.60,9.36;6,56.50,189.23,133.47,9.36", + ), + Text( + content="The students involved in this study are already familiar with reflecting and sharing their opinion, since it plays a vital role in their curriculum.", + document_order=194, + coordinates="6,193.19,189.23,95.19,9.36;6,56.50,201.24,231.89,9.36;6,56.50,213.25,231.88,9.36;6,56.50,225.26,49.64,9.36", + ), + Text( + content="On a national level there is a lively debate on end-of-life care, but not on the spiritual dimension.", + document_order=195, + coordinates="6,108.94,225.26,179.46,9.36;6,56.50,237.27,231.89,9.36", + ), + Text( + content="Therefore, an emphasis was placed on the spiritual dimension.", + document_order=196, + coordinates="6,56.51,249.28,231.88,9.36;6,56.51,261.29,47.17,9.36", + ), + Text( + content="This study has several practical implications.", + document_order=197, + coordinates="6,65.51,273.30,196.78,9.36", + ), + Text( + content="First, it pays off to integrate the program horizontally and vertically into the curriculum.", + document_order=198, + coordinates="6,266.39,273.30,22.01,9.36;6,56.51,285.31,231.87,9.36;6,56.51,297.32,160.16,9.36", + ), + Text( + content="It depends on the specific tasks and the curriculum itself how and where the integration can best take place.", + document_order=199, + coordinates="6,222.91,297.32,65.48,9.36;6,56.51,309.33,231.90,9.36;6,56.51,321.34,181.09,9.36", + ), + Text( + content="Some tasks will fit in well with education regarding communication or where clinical conditions are discussed such as oncology or lung disease.", + document_order=200, + coordinates="6,240.93,321.34,47.47,9.36;6,56.51,333.35,231.90,9.36;6,56.51,345.36,231.87,9.36;6,56.51,357.37,109.08,9.36", + ), + Text( + content="To avoid fragmentation and guarantee that all aspects are covered, however, it is also important that someone has an overview of where and how palliative care is addressed in the curriculum, for example, a curriculum coordinator or someone specifically assigned with this task.", + document_order=201, + coordinates="6,168.62,357.37,119.77,9.36;6,56.51,369.38,231.90,9.36;6,56.51,381.39,231.88,9.36;6,56.51,393.40,231.89,9.36;6,56.51,405.41,231.88,9.36;6,56.51,417.42,153.03,9.36", + ), + Text( + content="Furthermore, the tasks can always be adapted to specific contexts.", + document_order=202, + coordinates="6,213.64,417.42,74.76,9.36;6,56.51,429.43,213.40,9.36", + ), + Text( + content="For example, it might be difficult to arrange interviews with palliative care patients.", + document_order=203, + coordinates="6,273.72,429.43,14.69,9.36;6,56.51,441.44,231.89,9.36;6,56.51,453.45,119.44,9.36", + ), + Text( + content="Students could then interview a chronically ill patient instead of a palliative patient.", + document_order=204, + coordinates="6,178.45,453.45,109.95,9.36;6,56.51,465.46,231.90,9.36;6,56.51,477.47,32.57,9.36", + ), + Text( + content="With explicit attention to communication and spiritual care education, it is possible to better prepare students for working in the professional field.", + document_order=205, + coordinates="6,91.84,477.47,196.56,9.36;6,56.51,489.48,231.88,9.36;6,56.51,501.49,209.89,9.36", + ), + Text( + content="The spiritual dimension of care deserves explicit attention in the medical curriculum.", + document_order=206, + coordinates="6,271.55,501.49,16.86,9.36;6,56.51,513.50,231.89,9.36;6,56.51,525.51,112.93,9.36", + ), + Text( + content=", 2022 by guest.", + document_order=207, + coordinates="5,581.69,629.05,2.77,12.51;5,576.14,644.06,8.33,20.02;5,576.14,666.58,8.33,9.50;5,576.14,678.58,8.33,24.52", + ), + Text( + content="Protected by http://spcare.bmj.com/", + document_order=208, + coordinates="5,576.14,705.60,8.33,38.52;5,576.14,746.62,8.33,9.50;5,576.14,490.99,8.33,88.53", + ), + Text( + content="Original research", + document_order=209, + coordinates="6,60.50,24.52,85.87,9.48", + ), + Text( + content="The eight learning tasks", + document_order=210, + coordinates="2,348.73,604.84,81.98,9.01", + ), + Text( + content="The incorporated educational principles in the different learning tasks", + document_order=211, + coordinates="3,98.34,541.72,188.43,9.01;3,60.50,552.72,47.85,9.01", + ), + Text( + content="Themes and quotes of the stakeholders this(authenticity, eds.)in so far that patients tell you a lot of stories.", + document_order=212, + coordinates="5,98.34,50.36,134.78,9.01;5,145.80,80.06,206.18,8.06", + ), + Text( + content="So, in that sense, they are all very authentic stories and this is how patients present themselves to doctors'-I10'If you look at the learning tasks, you do see a clear structure.", + document_order=213, + coordinates="5,353.71,80.06,182.09,8.06;5,108.50,89.56,161.34,8.06;5,108.50,118.06,189.47,8.06", + ), + Text( + content="From watching to an increasingly active role to eventually actually having a conversation with a patient, of course'-I8", + document_order=214, + coordinates="5,299.70,118.06,231.07,8.06;5,108.50,127.57,130.78,8.06", + ), + Text( + content="We thank Judith Westen and Jimmy Frerejean for their support in designing these learning tasks.", + document_order=216, + coordinates="6,130.65,548.07,129.88,7.96;6,56.50,557.58,218.13,7.96", + ), + Text( + content="We further thank the involved students, teachers and educational scientist.", + document_order=217, + coordinates="6,276.96,557.58,11.43,7.96;6,56.50,567.09,222.78,7.96;6,56.50,576.60,31.41,7.96", + ), + Text( + content="Contributors DV and FW developed the learning tasks; JP and EN conducted the data collection.", + document_order=218, + coordinates="6,56.50,590.11,227.18,8.07;6,56.50,599.62,124.24,7.96", + ), + Text( + content="JP, EN and FW interpreted the data.", + document_order=219, + coordinates="6,183.11,599.62,97.60,7.96;6,56.50,609.13,31.66,7.96", + ), + Text( + content="JP drafted the manuscript; DV, MvdBvE and DD.", + document_order=220, + coordinates="6,90.53,609.13,179.27,7.96", + ), + Text( + content="supervised the study process.", + document_order=221, + coordinates="6,56.50,618.64,105.25,7.96", + ), + Text( + content="All authors have read and agreed to the final version of the manuscript.", + document_order=222, + coordinates="6,164.11,618.64,119.80,7.96;6,56.50,628.15,137.67,7.96", + ), + Text( + content="Funding This work was supported by ZonMW (project number 80-84400-98-027).", + document_order=223, + coordinates="6,56.50,641.66,231.68,8.07;6,56.50,651.17,70.40,7.96", + ), + Text( + content="Patient consent for publication Not required.", + document_order=225, + coordinates="6,56.50,678.18,164.07,8.07", + ), + Text( + content="Board (NVMO-ERB file 2020.5.3).", + document_order=227, + coordinates="6,56.50,710.71,128.95,7.96", + ), + Text( + content="Written consent was obtained from all participants.", + document_order=228, + coordinates="6,187.82,710.71,73.58,7.96;6,56.50,720.23,110.46,7.96", + ), + Text( + content="The participants gave their permission to use anonymised quotes.", + document_order=229, + coordinates="6,169.32,720.23,97.51,7.96;6,56.50,729.74,137.92,7.96", + ), + Text( + content="Provenance and peer review Not commissioned; externally peer reviewed.", + document_order=230, + coordinates="6,306.88,45.02,215.24,8.07;6,306.88,54.66,53.42,7.96", + ), + Text( + content="Commercial (CC BY-NC 4.0) license, which permits others to distribute, remix, adapt, build upon this work noncommercially, and license their derivative works on different terms, provided the original work is properly cited, appropriate credit is given, any changes made indicated, and the use is noncommercial.", + document_order=233, + coordinates="6,306.88,109.36,214.63,7.96;6,306.88,118.49,195.94,7.96;6,306.88,127.63,220.30,7.96;6,306.88,136.77,231.89,7.96;6,306.88,145.91,230.06,7.96;6,306.88,155.04,44.63,7.96", + ), + Text( + content="See: http:// creativecommons.", + document_order=234, + coordinates="6,353.87,155.04,105.68,7.96", + ), + Text( + content="org/ licenses/ by-nc/ 4. 0/.", + document_order=235, + coordinates="6,459.54,155.04,73.63,7.96;6,306.88,164.18,9.45,7.96", + ), + Text( + content="Jolien Pieters http:// orcid.", + document_order=237, + coordinates="6,306.88,191.46,94.21,7.96", + ), + Text( + content="org/ 0000-0002-9327-3977", + document_order=238, + coordinates="6,401.10,191.46,98.73,7.96", + ), +] diff --git a/good_ai/tests/sus/test_clean.py b/good_ai/tests/sus/test_clean.py new file mode 100644 index 0000000..bafea9c --- /dev/null +++ b/good_ai/tests/sus/test_clean.py @@ -0,0 +1,103 @@ +import unittest + +from src.sus.clean import clean + + +class TestClean(unittest.TestCase): + def test_xml_handling(self) -> None: + xml = 'Hi, my name
is András! <3 <> < ><> <> <|' + clean_xml = "Hi, my name is András! <3 <> <|" + clean_xml_ascii = "Hi, my name is Andras! <3 <> <|" + + self.assertEqual(clean(xml), clean_xml) + self.assertEqual(clean(xml, ignore_xml=True, ignore_latex=True), xml) + self.assertEqual(clean(xml, convert_to_ascii=True), clean_xml_ascii) + + def test_simple_latex_handling(self) -> None: + latex = 'Bj\\"{o}rn is \\textit{happy} 🙂' + clean_latex = "Björn is happy 🙂" + clean_latex_ascii = "Bjorn is happy" + + self.assertEqual(clean(latex), clean_latex) + self.assertEqual(clean(latex, ignore_latex=True), latex) + self.assertEqual(clean(latex, convert_to_ascii=True), clean_latex_ascii) + + def test_cursed(self) -> None: + cursed = """ + t̴̨̢̝̻͚͓͉̀̄̃́͜o̸͉̰̼͖͖̅̀̓̿̇̚͝ ̶̢͕̄͊̾͑̂̀̉͑ṱ̸̨̛͈̣̠̤͍̰̂̅͋̀́h̸̹̐͗̅̉͐͂͝e̶̜̳̞̍͘ ̴̟̗̻̤̤̮̒̂̋̓́͐͘͜m̵̺̫̥̙̣̥͐̌͜o̴̖͙̙̎̒͂o̷̬̤͑̆̂̉̊̂n̸̥͒̊ + """ + cleaned = "to the moon" + + self.assertEqual(clean(cursed), cursed.strip()) + self.assertEqual(clean(cursed, convert_to_ascii=True), cleaned) + + def test_whitespace(self) -> None: + text = """ + + word1 + + word2 \n\n\n\t + wo\t\f rd3 + + + """ # noqa: W293 + cleaned = "word1 word2 wo rd3" + + self.assertEqual(clean(text, convert_to_ascii=True), cleaned) + self.assertEqual(clean(text, ignore_xml=True, ignore_latex=True), cleaned) + self.assertEqual(clean(text), cleaned) + + def test_hyphens(self) -> None: + text = """ + break - word + break- word + break -word + break -word + break-\tword + """ + cleaned = "break - word break-word break-word break-word break-word" + + self.assertEqual(clean(text), cleaned) + + def test_bracket_removal(self) -> None: + text = "something [0], and [frefe, ferf]" + cleaned = "something, and" + + self.assertEqual(clean(text, remove_brackets=True), cleaned) + self.assertEqual( + clean(text, ignore_xml=True, ignore_latex=True, remove_brackets=True), + cleaned, + ) + self.assertEqual( + clean(text, convert_to_ascii=True, remove_brackets=True), cleaned + ) + self.assertEqual(clean(text), text) + + def test_latex_math_handling(self) -> None: + latex = "An increase of 3% was achieved with $q_1 = \\frac{3}{5}$." + bad_latex = "An increase of 3% was achieved with $q_1 = \\frac{3}/5$." + clean_latex = "An increase of 3% was achieved with q_1 = 3/5." + clean_bad_latex = "An increase of 3% was achieved with q_1 = 3//5." + + self.assertEqual(clean(latex), clean_latex) + self.assertEqual(clean(bad_latex), clean_bad_latex) + self.assertEqual(clean(latex, ignore_latex=True), latex) + self.assertEqual(clean(bad_latex, ignore_latex=True), bad_latex) + + def test_everything(self) -> None: + text = """ + Hi % 3 >2

my paper

there! cost- effective cost - effective cost -effective \\frac{1/2} hi \\frac{1}{2} \\textit{italic} \\`acc\\^ented text 北亰 fi æ IJ ij ff András öô 2132 rgrv \n\\   fd [32] [Bei et al., 2003]\n 🤡 🙄 😶‍🌫️ 🇲🇲 + """ + cleaned = "Hi% 3 >2 my paper there! cost-effective cost - effective cost-effective 1/2/hi 1/2 italic accented text Bei Jing fi ae IJ ij ff Andras oo 2132 rgrv fd" + + self.assertEqual( + clean(text, convert_to_ascii=True, remove_brackets=True), cleaned + ) + + def test_empty(self) -> None: + self.assertEqual(clean("", convert_to_ascii=True), "") + + def test_punctuation_fixing(self) -> None: + text = " Dear reader ( or listener ) , welcome ! " + fixed = "Dear reader (or listener), welcome!" + self.assertEqual(clean(text), fixed) diff --git a/good_ai/tests/sus/test_evaluate_ranking.py b/good_ai/tests/sus/test_evaluate_ranking.py new file mode 100644 index 0000000..fd8d20d --- /dev/null +++ b/good_ai/tests/sus/test_evaluate_ranking.py @@ -0,0 +1,93 @@ +import unittest +from pathlib import Path + +from src.sus.evaluate_ranking import evaluate_ranking + + +class TestEvaluateRanking(unittest.TestCase): + def test_default(self) -> None: + results = evaluate_ranking( + ["a", "a", "b", "b", "c", "d", "d", "d"], + [10, 7, 11, 6, 8, 2, 7, 1], + target_recall=0.6, + reverse_order=True, + ) + + self.assertEqual( + results, + {"d": 1.0, "c": 0.6666666666666666, "b": 0.4}, + ) + + results = evaluate_ranking( + ["a", "a", "b", "b", "c", "d", "d", "d"], + [10, 7, 11, 6, 8, 2, 7, 1], + target_recall=0.6, + reverse_order=False, + disable_interpolation=True, + ) + + self.assertEqual( + results, + {"a": 0.6666666666666666, "b": 0.3333333333333333, "c": 0.2857142857142857}, + ) + + def test_mismatching_lengths(self) -> None: + self.assertRaises( + ValueError, + evaluate_ranking, + ["a", "a", "b", "b", "c"], + [10, 7, 11, 6, 8, 2, 7, 1], + target_recall=0.6, + ) + + def test_invalid_recalls(self) -> None: + self.assertRaises( + AssertionError, + evaluate_ranking, + ["a", "a", "b", "b"], + [10, 7, 11, 6], + target_recall=10.6, + ) + + self.assertRaises( + AssertionError, + evaluate_ranking, + ["a", "a", "b", "b"], + [10, 7, 11, 6], + target_recall=-0.0001, + ) + + self.assertRaises( + AssertionError, + evaluate_ranking, + ["a", "a", "b", "b"], + [10, 7, 11, 6], + target_recall=1.00001, + ) + + def test_empty(self) -> None: + evaluate_ranking( + [], + [], + target_recall=0.6, + ) + + evaluate_ranking( + ["a"], + [1], + target_recall=0.6, + ) + + def test_save(self) -> None: + path = Path("test.svg") + path.unlink(missing_ok=True) + + evaluate_ranking( + ["a", "a", "b", "b", "c", "d", "d", "d"], + [10, 7, 11, 6, 8, 2, 7, 1], + target_recall=0.1, + output_svg=path, + ) + + self.assertTrue(path.exists()) + path.unlink() diff --git a/good_ai/tests/sus/test_get_sentences.py b/good_ai/tests/sus/test_get_sentences.py new file mode 100644 index 0000000..4146811 --- /dev/null +++ b/good_ai/tests/sus/test_get_sentences.py @@ -0,0 +1,16 @@ +import unittest + +from src.sus.get_sentences import get_sentences + + +class TestGetSentences(unittest.TestCase): + def test_default(self) -> None: + text = "This is a complete sentence. So is this. However this is n" # ot. + expected = ["This is a complete sentence.", "So is this.", "However this is n"] + + self.assertEqual(get_sentences(text), expected) + self.assertEqual(get_sentences(text, ignore_partial=True), expected[0:2]) + + def test_empty(self) -> None: + self.assertEqual(get_sentences(""), []) + self.assertEqual(get_sentences("", ignore_partial=True), []) diff --git a/good_ai/tests/sus/test_language.py b/good_ai/tests/sus/test_language.py new file mode 100644 index 0000000..84d1093 --- /dev/null +++ b/good_ai/tests/sus/test_language.py @@ -0,0 +1,32 @@ +import unittest + +from src.sus.language import english_name_of_language, is_english, predict_language + + +class TestLanguage(unittest.TestCase): + def test_predict_language(self) -> None: + self.assertEqual(predict_language("This is an English text."), "en") + self.assertEqual(predict_language("Ez egy magyar szöveg."), "hu") + self.assertEqual(predict_language("此處按原典,應為「黃武元年」,而電子稿此處為「黃初元年」。"), "zh-TW") + self.assertEqual(predict_language("32"), "und") + self.assertEqual(predict_language(""), "und") + + def test_is_english(self) -> None: + self.assertTrue(is_english("en")) + self.assertTrue(is_english("en-US")) + self.assertFalse(is_english("hu")) + self.assertFalse(is_english("de")) + self.assertFalse(is_english("zh")) + self.assertFalse(is_english("zh-TW")) + self.assertFalse(is_english("und")) + self.assertFalse(is_english("")) + self.assertFalse(is_english(None)) + + def english_name_of_language(self) -> None: + self.assertEqual(english_name_of_language("en"), "English") + self.assertEqual(english_name_of_language("hu"), "Hungarian") + self.assertEqual(english_name_of_language("zh"), "Chinese") + self.assertEqual(english_name_of_language("zh-TW"), "Chinese") + self.assertEqual(english_name_of_language("und"), "Unknown language") + self.assertEqual(english_name_of_language(""), "Unknown language") + self.assertEqual(english_name_of_language(None), "Unknown language") diff --git a/good_ai/tests/sus/test_lemmatize_text.py b/good_ai/tests/sus/test_lemmatize_text.py new file mode 100644 index 0000000..99da45b --- /dev/null +++ b/good_ai/tests/sus/test_lemmatize_text.py @@ -0,0 +1,139 @@ +import unittest + +from src.sus.lemmatize_text import lemmatize_text + + +class TestLemmatizeText(unittest.TestCase): + def test_simple(self) -> None: + text = "The state-of-the-art could not be improved, however we managed to create a less resource-intensive implementation of it." + + lemmatized = [ + "the", + "state", + "-", + "of", + "-", + "the", + "-", + "art", + "could", + "not", + "be", + "improve", + ",", + "however", + "we", + "manage", + "to", + "create", + "a", + "less", + "resource", + "-", + "intensive", + "implementation", + "of", + "it", + ".", + ] + + lemmatized_pos = [ + "the_DET", + "state_NOUN", + "-_PUNCT", + "of_ADP", + "-_PUNCT", + "the_DET", + "-_PUNCT", + "art_NOUN", + "could_AUX", + "not_PART", + "be_AUX", + "improve_VERB", + ",_PUNCT", + "however_ADV", + "we_PRON", + "manage_VERB", + "to_PART", + "create_VERB", + "a_DET", + "less_ADJ", + "resource_NOUN", + "-_PUNCT", + "intensive_ADJ", + "implementation_NOUN", + "of_ADP", + "it_PRON", + "._PUNCT", + ] + + lemmatized_neg = [ + "the", + "state", + "-", + "of", + "-", + "the", + "-", + "art", + "could", + "not", + "NOT_be", + "NOT_improve", + "NOT_,", + "however", + "we", + "manage", + "to", + "create", + "a", + "less", + "resource", + "-", + "intensive", + "implementation", + "of", + "it", + ".", + ] + + lemmatized_pos_neg = [ + "the_DET", + "state_NOUN", + "-_PUNCT", + "of_ADP", + "-_PUNCT", + "the_DET", + "-_PUNCT", + "art_NOUN", + "could_AUX", + "not_PART", + "NOT_be_AUX", + "NOT_improve_VERB", + "NOT_,_PUNCT", + "however_ADV", + "we_PRON", + "manage_VERB", + "to_PART", + "create_VERB", + "a_DET", + "less_ADJ", + "resource_NOUN", + "-_PUNCT", + "intensive_ADJ", + "implementation_NOUN", + "of_ADP", + "it_PRON", + "._PUNCT", + ] + + self.assertEqual(lemmatize_text(text), lemmatized) + self.assertEqual(lemmatize_text(text, add_part_of_speech=True), lemmatized_pos) + self.assertEqual(lemmatize_text(text, add_negation=True), lemmatized_neg) + self.assertEqual( + lemmatize_text(text, add_negation=True, add_part_of_speech=True), + lemmatized_pos_neg, + ) + + def test_empty(self) -> None: + self.assertEqual(lemmatize_text(""), []) diff --git a/good_ai/tests/sus/test_lemmatize_token.py b/good_ai/tests/sus/test_lemmatize_token.py new file mode 100644 index 0000000..18bd1f3 --- /dev/null +++ b/good_ai/tests/sus/test_lemmatize_token.py @@ -0,0 +1,20 @@ +import unittest + +from src.sus.lemmatize_text import lemmatize_token +from src.sus.nlp import nlp + + +class TestLemmatizeToken(unittest.TestCase): + def test_simple(self) -> None: + token = nlp("Center")[0] + + self.assertEqual(lemmatize_token(token), "centre") + self.assertEqual(lemmatize_token(token, add_negation=True), "centre") + self.assertEqual(lemmatize_token(token, add_part_of_speech=True), "centre_NOUN") + + def test_punctuation(self) -> None: + token = nlp("This.")[1] + + self.assertEqual(lemmatize_token(token), ".") + self.assertEqual(lemmatize_token(token, add_negation=True), ".") + self.assertEqual(lemmatize_token(token, add_part_of_speech=True), "._PUNCT") diff --git a/good_ai/tests/sus/test_match_names.py b/good_ai/tests/sus/test_match_names.py new file mode 100644 index 0000000..893f24c --- /dev/null +++ b/good_ai/tests/sus/test_match_names.py @@ -0,0 +1,71 @@ +import unittest + +from src.sus.match_names.match_names import match_names + + +class TestMatchNames(unittest.TestCase): + def test_grid(self) -> None: + names = [ + ["Al-Nasiry, S.", "Al-Nasiry S"], + ["De Leo, A. (Andreina)"], + [ + "DeBenedictis, J.N. (Julia)", + "DeBenedictis, N. (Julia)", + "DeBenedictis, J.", + "Julia DeBenedictis", + ], + ["Diesen, J.A.Y. van"], + ["Nio, C Yung"], + ["Uyen Chau Nguyen"], + ["Hagger M.S.", "Martin Hagger"], + ["Izabela-Cristina STANCU"], + ["Verborgh R.", "Ruben Verborgh"], + ["Droshout, D.T.G.G.M.L. (Dimitri)"], + ["El Demellawy"], + ["Sebastiaan van de Velde", "van de Velde, Sebastiaan"], + ["Sebastiaan Brand"], + ["Bertil FM Blok", "B.F.M. Blok"], + ["Bob Zadok Blok"], + ["Shannon Spruit"], + ["Shannon Kroes"], + [ + "MSc Jérémie Decouchant", + "PhD, Jérémie Decouchant", + "PhD Jérémie Decouchant", + "Jérémie Decouchant", + "MD, PhD Jérémie Decouchant", + ], + ["Jeremie Gobeil"], + ["Wouters, B.B.R.E.F.", "Wouters B.B. R. E.F."], + ["Elias Caldeira Dantas, A.M. (Aline)"], + ["Ed Harris"], + ["Ed Deprettere ", " Ed Deprettere "], + ["András Schmelczer"], + ["Enden, G. van den (Gitta)"], + ["Ruben van Dijk"], + ["Richard van Dijk"], + ["Xinping Guan", "X. Guan"], + ["Xiaohong Guan", "X. Guan"], + ["Pruimboom, Tim", "Pruimboom T."], + ["Sanchez-Faddeev H.", "Sanchez-Faddeev, Hernando"], + ["Duijnhoven - Jansen, E.M. van", "Emma M. van Jansen"], + ] + + all_names = [n for t in names for n in t] + + for n1 in all_names: + for n2 in all_names: + is_match = match_names(n1, n2) + groups = [t for t in names if n1 in t] + with self.subTest(n1=n1, n2=n2): + if any(n2 in g for g in groups): + self.assertTrue(is_match, "false negative") + elif all(n2 not in g for g in groups): + self.assertFalse(is_match, "false match") + + def test_empty(self) -> None: + self.assertFalse(match_names("", "")) + self.assertFalse(match_names(None, "")) + self.assertFalse(match_names(None, None)) + self.assertFalse(match_names("", None)) + self.assertFalse(match_names("Oliver", None)) diff --git a/good_ai/tests/sus/test_parallel_map.py b/good_ai/tests/sus/test_parallel_map.py new file mode 100644 index 0000000..0664e14 --- /dev/null +++ b/good_ai/tests/sus/test_parallel_map.py @@ -0,0 +1,42 @@ +import unittest + +from src.sus.parallel_map import parallel_map + +COUNT = int(1e5) + 3 + + +class TestParallelMap(unittest.TestCase): + def test_simple_case_with_progress_bar(self) -> None: + inputs = range(COUNT) + expected = [v**2 for v in range(COUNT)] + + self.assertEqual(parallel_map(lambda v: v**2, inputs), expected) + + def test_simple_case_without_progress_bar(self) -> None: + inputs = range(COUNT) + expected = [v**2 for v in range(COUNT)] + + self.assertEqual( + parallel_map(lambda v: v**2, inputs, disable_progress=True), expected + ) + + def test_simple_case_invalid_values(self) -> None: + inputs = range(COUNT) + + self.assertRaises( + AssertionError, parallel_map, lambda v: v**2, inputs, concurrency=0 + ) + self.assertRaises( + AssertionError, parallel_map, lambda v: v**2, inputs, chunk_size=0 + ) + + def test_no_op(self) -> None: + self.assertEqual(parallel_map(lambda v: v**2, [], disable_progress=True), []) + self.assertEqual( + parallel_map(lambda v: v**2, [], disable_progress=True, chunk_size=100), + [], + ) + self.assertEqual( + parallel_map(lambda v: v**2, [], disable_progress=True, concurrency=100), + [], + ) diff --git a/good_ai/tests/sus/test_publication_tei.py b/good_ai/tests/sus/test_publication_tei.py new file mode 100644 index 0000000..82c8814 --- /dev/null +++ b/good_ai/tests/sus/test_publication_tei.py @@ -0,0 +1,44 @@ +import unittest +from pathlib import Path + +from src.sus.publication_tei import PublicationTEI + +from .data.parsed import authors, content, metadata, sentences + +DATA_PATH = Path(__file__).parent.resolve() / "data" + + +class TestPublicationTEI(unittest.TestCase): + test_xml: str + + @classmethod + def setUpClass(cls) -> None: + with open( + DATA_PATH / "10.1136_bmjspcare-2021-003026.pdf.tei.xml", encoding="utf-8" + ) as f: + cls.test_xml = f.read() + + def test_metadata_extraction(self) -> None: + self.assertEqual(PublicationTEI(self.test_xml).publication_metadata, metadata) + + def test_authors(self) -> None: + self.assertEqual(PublicationTEI(self.test_xml).authors, authors) + + def test_content(self) -> None: + self.assertEqual(PublicationTEI(self.test_xml).content, content) + + def test_sentences(self) -> None: + self.assertEqual(PublicationTEI(self.test_xml).sentences, sentences) + + def test_empty1(self) -> None: + tei = PublicationTEI("") + tei.publication_metadata, tei.publication_metadata, tei.authors, tei.content, tei.sentences + + def test_empty2(self) -> None: + tei = PublicationTEI("") + tei.publication_metadata, tei.publication_metadata, tei.authors, tei.content, tei.sentences + + def test_missing_fields(self) -> None: + with open(DATA_PATH / "bad.tei.xml", encoding="utf-8") as f: + tei = PublicationTEI(f.read()) + tei.publication_metadata, tei.publication_metadata, tei.authors, tei.content, tei.sentences diff --git a/good_ai/tests/sus/test_unique.py b/good_ai/tests/sus/test_unique.py new file mode 100644 index 0000000..8b90e08 --- /dev/null +++ b/good_ai/tests/sus/test_unique.py @@ -0,0 +1,54 @@ +import unittest + +from src.sus.unique import unique + +original = [ + ("a", 1), + ("b", 5), + ("a", -3), + ("c", 5), + ("d", 2), + ("a", 2), +] + + +class TestUnique(unittest.TestCase): + def test_default(self) -> None: + values = [ + *original, + ("a", 2), + ("a", 1), + ("a", -3), + ("b", 5), + ("c", 5), + ("d", 2), + ] + + expected = original + + self.assertEqual(unique(values), expected) + self.assertEqual(unique(values, key=lambda v: v), expected) + + def test_with_key_0(self) -> None: + values = original + + expected = [ + ("a", 1), + ("b", 5), + ("c", 5), + ("d", 2), + ] + + self.assertEqual(unique(values, key=lambda v: v[0]), expected) + + def test_with_key_1(self) -> None: + values = original + + expected = [ + ("a", 1), + ("b", 5), + ("a", -3), + ("d", 2), + ] + + self.assertEqual(unique(values, key=lambda v: v[1]), expected)