├── .flake8 ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .travis.yml ├── .yamllint ├── README.md ├── build ├── docopt.py └── packageWorkflow.py ├── docs ├── catalina1.png ├── catalina2.png ├── complexsearch.png ├── ecount.png ├── settings.png ├── taco.png └── tones.png ├── icon.png ├── images └── Checkmark.png ├── img └── .gitignore ├── info.plist ├── mypy.ini ├── na.png ├── pytest.ini ├── release.py ├── release.sh ├── requirements.txt ├── src ├── __init__.py ├── bg_downloader.py ├── buildDataFiles.py ├── downloadDataFiles.py ├── esearch.py ├── libs │ ├── __init__.py │ ├── bs4 │ │ ├── __init__.py │ │ ├── builder │ │ │ ├── __init__.py │ │ │ ├── _html5lib.py │ │ │ ├── _htmlparser.py │ │ │ └── _lxml.py │ │ ├── dammit.py │ │ ├── diagnose.py │ │ ├── element.py │ │ ├── formatter.py │ │ └── testing.py │ ├── certifi │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── cacert.pem │ │ └── core.py │ ├── charset_normalizer │ │ ├── __init__.py │ │ ├── api.py │ │ ├── assets │ │ │ └── __init__.py │ │ ├── cd.py │ │ ├── cli │ │ │ ├── __init__.py │ │ │ └── normalizer.py │ │ ├── constant.py │ │ ├── legacy.py │ │ ├── md.py │ │ ├── models.py │ │ ├── py.typed │ │ ├── utils.py │ │ └── version.py │ ├── idna │ │ ├── __init__.py │ │ ├── codec.py │ │ ├── compat.py │ │ ├── core.py │ │ ├── idnadata.py │ │ ├── intranges.py │ │ ├── package_data.py │ │ ├── py.typed │ │ └── uts46data.py │ ├── requests │ │ ├── __init__.py │ │ ├── __version__.py │ │ ├── _internal_utils.py │ │ ├── adapters.py │ │ ├── api.py │ │ ├── auth.py │ │ ├── certs.py │ │ ├── compat.py │ │ ├── cookies.py │ │ ├── exceptions.py │ │ ├── help.py │ │ ├── hooks.py │ │ ├── models.py │ │ ├── packages.py │ │ ├── sessions.py │ │ ├── status_codes.py │ │ ├── structures.py │ │ └── utils.py │ ├── soupsieve │ │ ├── __init__.py │ │ ├── __meta__.py │ │ ├── css_match.py │ │ ├── css_parser.py │ │ ├── css_types.py │ │ ├── pretty.py │ │ ├── py.typed │ │ └── util.py │ ├── urllib3 │ │ ├── __init__.py │ │ ├── _collections.py │ │ ├── _version.py │ │ ├── connection.py │ │ ├── connectionpool.py │ │ ├── contrib │ │ │ ├── __init__.py │ │ │ ├── _appengine_environ.py │ │ │ ├── _securetransport │ │ │ │ ├── __init__.py │ │ │ │ ├── bindings.py │ │ │ │ └── low_level.py │ │ │ ├── appengine.py │ │ │ ├── ntlmpool.py │ │ │ ├── pyopenssl.py │ │ │ ├── securetransport.py │ │ │ └── socks.py │ │ ├── exceptions.py │ │ ├── fields.py │ │ ├── filepost.py │ │ ├── poolmanager.py │ │ ├── request.py │ │ ├── response.py │ │ └── util │ │ │ ├── __init__.py │ │ │ ├── connection.py │ │ │ ├── proxy.py │ │ │ ├── queue.py │ │ │ ├── request.py │ │ │ ├── response.py │ │ │ ├── retry.py │ │ │ ├── ssl_.py │ │ │ ├── ssl_match_hostname.py │ │ │ ├── ssltransport.py │ │ │ ├── timeout.py │ │ │ ├── url.py │ │ │ └── wait.py │ └── workflow │ │ ├── Notify.tgz │ │ ├── __init__.py │ │ ├── background.py │ │ ├── notify.py │ │ ├── update.py │ │ ├── util.py │ │ ├── workflow.py │ │ └── workflow3.py └── magic.py ├── test_emoji.py └── workflow-build.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | per-file-ignores = 3 | src/*.py: E501,E722 4 | *.py: E501 5 | intellifire4py/__init__.py: F401, 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.9 3 | exclude: ^$ 4 | repos: 5 | - repo: 'https://github.com/pre-commit/pre-commit-hooks' 6 | rev: v4.1.0 7 | hooks: 8 | - id: trailing-whitespace 9 | exclude: src/libs|build 10 | - id: end-of-file-fixer 11 | exclude: src/libs|build 12 | - id: check-yaml 13 | exclude: src/libs|build 14 | - id: check-added-large-files 15 | exclude: src/libs|build 16 | - id: check-ast 17 | exclude: src/libs|build 18 | - id: check-builtin-literals 19 | exclude: src/libs|build 20 | - id: check-docstring-first 21 | exclude: src/libs|build 22 | - id: debug-statements 23 | exclude: src/libs|build 24 | - repo: 'https://github.com/codespell-project/codespell' 25 | rev: v2.1.0 26 | hooks: 27 | - id: codespell 28 | args: 29 | - >- 30 | --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,iif,ines,ist,lightsensor,mut,nd,pres,referer,rime,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort,ba,haa,pullrequests 31 | - '--skip="./.*,*.csv,*.json"' 32 | - '--quiet-level=2' 33 | exclude_types: 34 | - csv 35 | - json 36 | exclude: ^tests/fixtures/|src/libs|build 37 | - repo: 'https://github.com/asottile/pyupgrade' 38 | rev: v2.31.1 39 | hooks: 40 | - id: pyupgrade 41 | args: 42 | - '--py39-plus' 43 | exclude: src/libs|build 44 | - repo: 'https://github.com/PyCQA/isort' 45 | rev: 5.10.1 46 | hooks: 47 | - id: isort 48 | args: 49 | - '--profile' 50 | - black 51 | - '--filter-files' 52 | exclude: src/libs|build 53 | - repo: 'https://github.com/psf/black' 54 | rev: 22.3.0 55 | hooks: 56 | - id: black 57 | exclude: src/libs|build 58 | - repo: 'https://github.com/PyCQA/flake8' 59 | rev: 4.0.1 60 | hooks: 61 | - id: flake8 62 | additional_dependencies: 63 | - flake8-docstrings 64 | - pydocstyle 65 | - pycodestyle 66 | - flake8-comprehensions 67 | - flake8-noqa 68 | - mccabe 69 | exclude: src/libs|build|test_emoji.py 70 | - repo: 'https://github.com/pre-commit/mirrors-mypy' 71 | rev: v0.942 72 | hooks: 73 | - id: mypy 74 | types: 75 | - python 76 | additional_dependencies: 77 | - types-requests 78 | - pydantic 79 | exclude: src/libs|build 80 | - repo: 'https://github.com/Lucas-C/pre-commit-hooks-nodejs' 81 | rev: v1.1.2 82 | hooks: 83 | - id: markdown-toc 84 | args: 85 | - '--indent' 86 | - ' ' 87 | - '-i' 88 | - repo: https://github.com/pre-commit/pre-commit-hooks 89 | rev: v4.1.0 90 | hooks: 91 | - id: check-executables-have-shebangs 92 | stages: [manual] 93 | - id: check-json 94 | exclude: (.vscode|.devcontainer|src/libs|build) 95 | - repo: 'https://github.com/adrienverge/yamllint.git' 96 | rev: v1.26.3 97 | hooks: 98 | - id: yamllint 99 | - repo: https://github.com/pre-commit/mirrors-prettier 100 | rev: v2.6.2 101 | hooks: 102 | - id: prettier 103 | stages: [manual] 104 | exclude: src/libs|build 105 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | max-line-length=240 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | install: 5 | - pip install -r requirements.txt 6 | - pip install pytest-travis-fold 7 | script: pytest -l --capture=no --travis-fold 8 | notifications: 9 | email: 10 | recipients: 11 | - jeffstein@gmail.com 12 | - jstein@mitre.org 13 | on_success: never # default: change 14 | on_failure: always # default: always 15 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | ignore: | 2 | azure-*.yml 3 | rules: 4 | braces: 5 | level: error 6 | min-spaces-inside: 0 7 | max-spaces-inside: 1 8 | min-spaces-inside-empty: -1 9 | max-spaces-inside-empty: -1 10 | brackets: 11 | level: error 12 | min-spaces-inside: 0 13 | max-spaces-inside: 0 14 | min-spaces-inside-empty: -1 15 | max-spaces-inside-empty: -1 16 | colons: 17 | level: error 18 | max-spaces-before: 0 19 | max-spaces-after: 1 20 | commas: 21 | level: error 22 | max-spaces-before: 0 23 | min-spaces-after: 1 24 | max-spaces-after: 1 25 | comments: 26 | level: error 27 | require-starting-space: true 28 | min-spaces-from-content: 2 29 | comments-indentation: 30 | level: error 31 | document-end: 32 | level: error 33 | present: false 34 | document-start: 35 | level: error 36 | present: false 37 | empty-lines: 38 | level: error 39 | max: 1 40 | max-start: 0 41 | max-end: 1 42 | hyphens: 43 | level: error 44 | max-spaces-after: 1 45 | indentation: 46 | level: error 47 | spaces: 2 48 | indent-sequences: true 49 | check-multi-line-strings: false 50 | key-duplicates: 51 | level: error 52 | line-length: disable 53 | new-line-at-end-of-file: 54 | level: error 55 | new-lines: 56 | level: error 57 | type: unix 58 | trailing-spaces: 59 | level: error 60 | truthy: 61 | level: error 62 | allowed-values: ['true', 'false', 'on'] 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/jeeftor/EmojiTaco.svg?branch=master)](https://travis-ci.org/jeeftor/EmojiTaco) 2 | 3 | 4 | 5 | 6 | # Emoji Taco 🌮 (for python3 - Version 2.x) 7 | 8 | [**Download the latest release**](https://github.com/jeeftor/EmojiTaco/releases) 9 | 10 | 11 | ## Python 3 Notes 12 | 13 | * Notifications aren't currently working that I'm aware of - but this doesn't really impact users at all 14 | * Code-base *might* be smaller than Python2 version 15 | * Assumes you have a `python3` executable available 16 | 17 | 18 | 19 | ### Unicode website 20 | 21 | If you run in to trouble its possible the unicode website is down. You can check [here](https://downfor.io/unicode.org). _This was an issue 9/10 April 2020_ 22 | 23 | 38 | ## About 39 | This emoji workflow scrapes Unicode.org to pull down the latest set of EMOJI!!. On first run you **must** be connected to the Internet so that you can generate the initial Emoji set. As new emoji are released you should be able to re-generate to keep the plugin up to date. 40 | 41 | 47 | 48 | 49 | # To initialize the emoji set type 50 | 51 | **init emoji** this will go to `unicode.org` and scrape the current emoji list. It parses the mega chart pulling out all Emoji that exist in the apple ecosystem. 52 | 53 | # Emoji Searching 54 | 55 | Use the **e** command to search for emoji. 56 | ![tac](docs/taco.png) 57 | 58 | You can use the `-` to exclude items from a search such as: 59 | 60 | ![](docs/complexsearch.png) 61 | 62 | # Emoji Count 63 | If there are more than 9 matches a dialog showing the emoji count will appear. 64 | ![](docs/ecount.png) 65 | 66 | 67 | ### Commands 68 | 69 | * alt/option (**⌥**) - Show Unicode Values 70 | * cmd (**⌘**) - Show Python String 71 | * ctrl (**⌃**) - Show Python String *decoded* 72 | * shift (**⇧**) - Display the image in QuickLook 73 | 74 | 75 | 76 | # Non Apple-supported Emoji 77 | 78 | There are emoji that are not supported on Apple devices and/or not supported on OSX but maybe exist in a beta version of iOS. 79 | 80 | For example the Rainbow Flag emoji currently will render a pride flag on iOS but on OSX (at the time of this writing) it will render a while flag followed by a rainbow symbol 🏳️‍🌈️. You should still be able to use this and other emoji even if they render strangely on the current OS. 81 | 82 | # Configuration Options 83 | You can change the keyword in the settings of the workflow 84 | 85 | ![](docs/settings.png) 86 | -------------------------------------------------------------------------------- /build/packageWorkflow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2013 deanishe@deanishe.net. 4 | # 5 | # MIT Licence. See http://opensource.org/licenses/MIT 6 | # 7 | # Created on 2013-11-01 8 | # 9 | 10 | """workflow-build [options] 11 | 12 | Build Alfred Workflows. 13 | 14 | Compile contents of to a ZIP file (with extension 15 | `.alfredworkflow`). 16 | 17 | The name of the output file is generated from the workflow name, 18 | which is extracted from the workflow's `info.plist`. If a `version` 19 | file is contained within the workflow directory, it's contents 20 | will be appended to the compiled workflow's filename. 21 | 22 | Usage: 23 | workflow-build [-v|-q|-d] [-f] [-o ] ... 24 | workflow-build (-h|--version) 25 | 26 | Options: 27 | -o, --output= Directory to save workflow(s) to. 28 | Default is current working directory. 29 | -f, --force Overwrite existing files. 30 | -h, --help Show this message and exit. 31 | -V, --version Show version number and exit. 32 | -q, --quiet Only show errors and above. 33 | -v, --verbose Show info messages and above. 34 | -d, --debug Show debug messages. 35 | 36 | """ 37 | 38 | 39 | import sys 40 | import os 41 | import logging 42 | import logging.handlers 43 | import plistlib 44 | import semantic_version 45 | 46 | from subprocess import check_call, CalledProcessError 47 | 48 | from docopt import docopt 49 | 50 | __version__ = "0.2" 51 | __author__ = "deanishe@deanishe.net" 52 | 53 | DEFAULT_LOG_LEVEL = logging.WARNING 54 | LOGPATH = os.path.expanduser("~/Library/Logs/MyScripts.log") 55 | LOGSIZE = 1024 * 1024 * 5 # 5 megabytes 56 | 57 | 58 | EXCLUDE_PATTERNS = [ 59 | "*.pyc", 60 | "*.log", 61 | ".DS_Store", 62 | "*.acorn", 63 | "*.swp", 64 | "*.sublime-project", 65 | "*.sublime-workflow", 66 | "*.git", 67 | "*.dist-info", 68 | "*.idea", 69 | ".idea", 70 | ".git", 71 | "./eenv/*", 72 | "./img/*.png", 73 | "/build/*", 74 | "/sketch/*", 75 | "*.csv", 76 | "*.alfredworkflow", 77 | ] 78 | 79 | 80 | class TechnicolorFormatter(logging.Formatter): 81 | """ 82 | Prepend level name to any message not level logging.INFO. 83 | 84 | Also, colour! 85 | 86 | """ 87 | 88 | BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) 89 | 90 | RESET = "\033[0m" 91 | COLOUR_BASE = "\033[1;{:d}m" 92 | BOLD = "\033[1m" 93 | 94 | LEVEL_COLOURS = { 95 | logging.DEBUG: BLUE, 96 | logging.INFO: WHITE, 97 | logging.WARNING: YELLOW, 98 | logging.ERROR: MAGENTA, 99 | logging.CRITICAL: RED, 100 | } 101 | 102 | def __init__(self, fmt=None, datefmt=None, technicolor=True): 103 | logging.Formatter.__init__(self, fmt, datefmt) 104 | self.technicolor = technicolor 105 | self._isatty = sys.stderr.isatty() 106 | 107 | def format(self, record): 108 | if record.levelno == logging.INFO: 109 | msg = logging.Formatter.format(self, record) 110 | return msg 111 | if self.technicolor and self._isatty: 112 | colour = self.LEVEL_COLOURS[record.levelno] 113 | bold = (False, True)[record.levelno > logging.INFO] 114 | levelname = self.colourise(f"{record.levelname:9s}", colour, bold) 115 | else: 116 | levelname = f"{record.levelname:9s}" 117 | return levelname + logging.Formatter.format(self, record) 118 | 119 | def colourise(self, text, colour, bold=False): 120 | colour = self.COLOUR_BASE.format(colour + 30) 121 | output = [] 122 | if bold: 123 | output.append(self.BOLD) 124 | output.append(colour) 125 | output.append(text) 126 | output.append(self.RESET) 127 | return "".join(output) 128 | 129 | 130 | # logfile 131 | logfile = logging.handlers.RotatingFileHandler(LOGPATH, maxBytes=LOGSIZE, backupCount=5) 132 | formatter = logging.Formatter( 133 | "%(asctime)s %(levelname)-8s [%(name)-12s] %(message)s", datefmt="%d/%m %H:%M:%S" 134 | ) 135 | logfile.setFormatter(formatter) 136 | logfile.setLevel(logging.DEBUG) 137 | 138 | # console output 139 | console = logging.StreamHandler() 140 | formatter = TechnicolorFormatter("%(message)s") 141 | console.setFormatter(formatter) 142 | console.setLevel(logging.DEBUG) 143 | 144 | log = logging.getLogger("") 145 | log.addHandler(logfile) 146 | log.addHandler(console) 147 | 148 | 149 | def safename(name): 150 | """Make name filesystem-safe.""" 151 | name = name.replace("/", "-") 152 | name = name.replace(":", "-") 153 | return name 154 | 155 | 156 | def build_workflow(workflow_dir, outputdir, overwrite=False, verbose=False): 157 | """Create an .alfredworkflow file from the contents of `workflow_dir`.""" 158 | curdir = os.curdir 159 | os.chdir(workflow_dir) 160 | version = None 161 | if not os.path.exists("info.plist"): 162 | log.error("info.plist not found") 163 | return False 164 | 165 | if os.path.exists("version"): 166 | 167 | # Read version 168 | with open("version") as fp: 169 | initial_version = semantic_version.Version.coerce( 170 | fp.read().strip().decode("utf-8") 171 | ) 172 | fp.close() 173 | version = str(initial_version.next_patch()) 174 | 175 | target = open("version", "w") 176 | target.truncate() 177 | target.write(version) 178 | target.close() 179 | 180 | if not version: 181 | with open('info.plist', 'rb') as f: 182 | pl = plistlib.load(f) 183 | initial_version = semantic_version.Version.coerce(pl["version"]) 184 | version = str(initial_version.next_patch()) 185 | 186 | pl["version"] = version 187 | with open('info.plist', 'wb') as f: 188 | plistlib.dump(pl, f) 189 | 190 | # plistlib.writePlist(pl, "info.plist") 191 | with open('info.plist', 'rb') as f: 192 | pl = plistlib.load(f) 193 | name = safename(pl["name"]).replace(" ", "_") 194 | zippath = os.path.join(outputdir, name) 195 | 196 | if version: 197 | zippath += "-" + version 198 | zippath += ".alfredworkflow" 199 | 200 | if os.path.exists(zippath): 201 | if overwrite: 202 | log.info("Overwriting existing workflow") 203 | os.unlink(zippath) 204 | else: 205 | log.error(f"File '{zippath}' already exists. Use -f to overwrite") 206 | return False 207 | 208 | # build workflow 209 | command = ["zip"] 210 | if not verbose: 211 | command.append("-q") 212 | command.append(zippath) 213 | for root, dirnames, filenames in os.walk("."): 214 | dirnames[:] = [d for d in dirnames if not d in [".git", ".idea"]] 215 | for filename in filenames: 216 | path = os.path.join(root, filename) 217 | command.append(path) 218 | command.append("-x") 219 | command.extend(EXCLUDE_PATTERNS) 220 | log.debug("command : {}".format(" ".join(command))) 221 | try: 222 | check_call(command) 223 | except CalledProcessError as err: 224 | log.error(f"zip returned : {err.returncode}") 225 | os.chdir(curdir) 226 | return False 227 | log.info(f"Wrote {zippath}") 228 | # Return workflow filename and actual filename 229 | print(name, os.path.basename(zippath), version) 230 | os.chdir(curdir) 231 | return True 232 | 233 | 234 | def main(args=None): 235 | """Run CLI.""" 236 | args = docopt(__doc__, version=__version__) 237 | 238 | if args.get("--verbose"): 239 | log.setLevel(logging.INFO) 240 | elif args.get("--quiet"): 241 | log.setLevel(logging.ERROR) 242 | elif args.get("--debug"): 243 | log.setLevel(logging.DEBUG) 244 | else: 245 | log.setLevel(DEFAULT_LOG_LEVEL) 246 | 247 | log.debug("Set log level to %s" % logging.getLevelName(log.level)) 248 | 249 | log.debug(f"args :\n{args}") 250 | 251 | # Build options 252 | outputdir = os.path.abspath(args.get("--output") or os.curdir) 253 | workflow_dirs = [os.path.abspath(p) for p in args.get("")] 254 | log.debug(f"outputdir : {outputdir}, workflow_dirs : {workflow_dirs}") 255 | errors = False 256 | verbose = False 257 | if log.level == logging.DEBUG: 258 | verbose = True 259 | 260 | # Build workflow(s) 261 | for path in workflow_dirs: 262 | ok = build_workflow(path, outputdir, args.get("--force"), verbose) 263 | if not ok: 264 | errors = True 265 | if errors: 266 | return 1 267 | return 0 268 | 269 | 270 | if __name__ == "__main__": 271 | sys.exit(main(sys.argv[1:])) 272 | -------------------------------------------------------------------------------- /docs/catalina1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/docs/catalina1.png -------------------------------------------------------------------------------- /docs/catalina2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/docs/catalina2.png -------------------------------------------------------------------------------- /docs/complexsearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/docs/complexsearch.png -------------------------------------------------------------------------------- /docs/ecount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/docs/ecount.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/docs/settings.png -------------------------------------------------------------------------------- /docs/taco.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/docs/taco.png -------------------------------------------------------------------------------- /docs/tones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/docs/tones.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/icon.png -------------------------------------------------------------------------------- /images/Checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/images/Checkmark.png -------------------------------------------------------------------------------- /img/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | show_error_codes = true 4 | follow_imports = silent 5 | ignore_missing_imports = true 6 | strict_equality = true 7 | warn_incomplete_stub = true 8 | warn_redundant_casts = true 9 | warn_unused_configs = true 10 | warn_unused_ignores = true 11 | check_untyped_defs = true 12 | disallow_incomplete_defs = true 13 | disallow_subclassing_any = true 14 | disallow_untyped_calls = false 15 | disallow_untyped_decorators = true 16 | disallow_untyped_defs = true 17 | no_implicit_optional = true 18 | warn_return_any = true 19 | warn_unreachable = true 20 | ;plugins = pydantic.mypy 21 | ; 22 | ;[pydantic-mypy] 23 | ;init_forbid_extra = True 24 | ;init_typed = True 25 | ;warn_required_dynamic_aliases = True 26 | ;warn_untyped_fields = True 27 | -------------------------------------------------------------------------------- /na.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/na.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = test_emoji.py 3 | -------------------------------------------------------------------------------- /release.py: -------------------------------------------------------------------------------- 1 | """Make a github release. 2 | 3 | Requires that you have a github access token as specified below in your home directory. 4 | """ 5 | import json 6 | import os 7 | import sys 8 | import urllib 9 | import urllib.request 10 | from os.path import expanduser 11 | from subprocess import call 12 | 13 | GITHUB_USER = "jeeftor" 14 | GITHUB_REPO = "EmojiTaco" 15 | 16 | # Read github access token out of ~/.github_access_token 17 | home = expanduser("~") 18 | token_file = home + "/.github_access_token" 19 | GITHUB_ACCESS_TOKEN = open(token_file).read() 20 | 21 | 22 | def pp_json(json_thing, sort=True, indents=4): 23 | """Pretty print json.""" 24 | if type(json_thing) is str: 25 | print(json.dumps(json.loads(json_thing), sort_keys=sort, indent=indents)) 26 | else: 27 | print(json.dumps(json_thing, sort_keys=sort, indent=indents)) 28 | return None 29 | 30 | 31 | print(sys.argv) 32 | 33 | version = sys.argv[1] 34 | file_to_upload = sys.argv[2] 35 | 36 | github_token = str(GITHUB_ACCESS_TOKEN).rstrip() 37 | 38 | 39 | # curl -i -H 'Authorization: token 5b8e3a4d92993282d2a8f20b5fe4910edc9f82dd' https://api.github.com/user/repos 40 | 41 | request_headers = { 42 | "Content-Type": "application/json", 43 | "Authorization": "token %s" % github_token, 44 | } 45 | 46 | 47 | print(request_headers) 48 | 49 | # Release INFO 50 | payload = { 51 | "tag_name": f"v{version}", 52 | "target_commitish": "master", 53 | "name": f"Release {version}", 54 | "body": "Auto Generated Release notes by the `release.py` script", 55 | "draft": True, 56 | "prerelease": False, 57 | } 58 | 59 | 60 | # Make a new release 61 | data = json.dumps(payload) 62 | clen = len(data) 63 | request_headers["Content-Length"] = clen 64 | url = f"https://api.github.com/repos/{GITHUB_USER}/{GITHUB_REPO}/releases" 65 | # url = 'https://api.github.com/repos/jeeftor/EmojiTaco/releases' 66 | print(url) 67 | req = urllib.request.Request(url, data=data, headers=request_headers) 68 | f = urllib.request.urlopen(req) 69 | response = f.read() 70 | f.close() 71 | pp_json(response) 72 | json = json.loads(response) 73 | 74 | # Parse out the upload URL 75 | url = json["upload_url"].split("{")[0] 76 | 77 | # Do more parsing 78 | upload_path = "build/" + file_to_upload 79 | upload_data_len = length = os.path.getsize(upload_path) 80 | upload_data = open(upload_path, "rb") 81 | url = url + f"?name={file_to_upload}" 82 | 83 | # Upload the new workflow file 84 | request = urllib.request.Request(url, data=upload_data, headers=request_headers) 85 | request.add_header("Cache-Control", "no-cache") 86 | request.add_header("Content-Length", "%d" % upload_data_len) 87 | res = urllib.request.urlopen(request).read().strip() 88 | 89 | 90 | # Launch web browser to the Draf release 91 | 92 | call(["open", json["html_url"]]) 93 | exit() 94 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | OUTPUT=$(python build/packageWorkflow.py . -o build/) 2 | #OUTPUT="Airport_Search Airport_Search-0.6.22.alfredworkflow 0.6.22" 3 | 4 | TEXT_ARRRAY=($OUTPUT) 5 | 6 | 7 | echo ${TEXT_ARRRAY[0]} 8 | echo ${TEXT_ARRRAY[1]} 9 | echo ${TEXT_ARRRAY[2]} 10 | 11 | echo " ________ __ _______ __ " 12 | echo " / ____/ /_ ____ _____ ____ ____ ____/ / / ____(_) /__ _____ " 13 | echo " / / / __ \/ __ \`/ __ \/ __ \`/ _ \/ __ / / /_ / / / _ \/ ___/ " 14 | echo " / /___/ / / / /_/ / / / / /_/ / __/ /_/ / / __/ / / / __(__ ) " 15 | echo " \____/_/ /_/\__,_/_/ /_/\__, /\___/\__,_/ /_/ /_/_/\___/____/ " 16 | echo " /____/ " 17 | echo "" 18 | 19 | git status -s 20 | 21 | echo " Commit and make a release?" 22 | 23 | read -p "Are you sure? " -n 1 -r 24 | echo # (optional) move to a new line 25 | if [[ $REPLY =~ ^[Yy]$ ]] 26 | then 27 | # do dangerous stuff 28 | 29 | git add -u 30 | git commit -m "Prepping release ${TEXT_ARRRAY[2]}" 31 | git push 32 | 33 | echo "Running..." 34 | echo python3 release.py ${TEXT_ARRRAY[2]} ${TEXT_ARRRAY[1]} 35 | echo "" 36 | python3 release.py ${TEXT_ARRRAY[2]} ${TEXT_ARRRAY[1]} 37 | fi 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # pytest==3.0.5 2 | # lxml==4.6.3 3 | # beautifulsoup4==4.5.3 4 | # pytest-catchlog 5 | 6 | bs4 7 | pickle 8 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """Package def file.""" 2 | -------------------------------------------------------------------------------- /src/downloadDataFiles.py: -------------------------------------------------------------------------------- 1 | """File Download wrapper.""" 2 | from __future__ import annotations 3 | 4 | import os 5 | import sys 6 | 7 | from workflow import Workflow3 8 | from workflow.background import is_running, run_in_background 9 | 10 | 11 | def string_from_percent(pct: float) -> str | None: 12 | """Return a fancy string to show in the workflow from the count item.""" 13 | # blue = "\\U0001F535\\U0000FE0F" 14 | white = "\U000026AA\U0000FE0F" 15 | black = "\U000026AB\U0000FE0F" 16 | 17 | ret: str = ( 18 | white 19 | + white 20 | + white 21 | + white 22 | + white 23 | + white 24 | + white 25 | + white 26 | + white 27 | + white 28 | + white 29 | + black 30 | + black 31 | + black 32 | + black 33 | + black 34 | + black 35 | + black 36 | + black 37 | + black 38 | + black 39 | ) 40 | 41 | mod = 2 * (10 - (int(pct / 10) % 10)) 42 | 43 | return ret[mod:][0:20] 44 | 45 | 46 | def build_wf_entry(wf: Workflow3) -> None: 47 | """Build workflow entry.""" 48 | if is_running("emoji_init"): 49 | """Update status.""" 50 | phase: str = wf.stored_data("phase") 51 | log.info("--dbg:\tIs Running with phase [%s]", phase) 52 | if phase != "done": 53 | wf.rerun = 0.5 54 | if phase == "downloading": 55 | pct = None 56 | while pct is None: 57 | try: 58 | pct = wf.stored_data("download_percent") 59 | except: 60 | pass 61 | 62 | progress = wf.stored_data("download_progress") 63 | file = wf.stored_data("download_file") 64 | 65 | title = f"Downloading {file} [{progress}]" 66 | subtitle = string_from_percent(pct) + " " + str(pct) + "%" 67 | wf.add_item(title, subtitle=subtitle) 68 | 69 | if phase == "processing": 70 | 71 | try: 72 | emoji_count = wf.stored_data("emoji_count") 73 | subtitle = f"Parsed {emoji_count} emoji" 74 | except Exception: 75 | subtitle = "Parsed ... emoji" 76 | pass 77 | 78 | title = "Parsing Emoji" 79 | wf.add_item(title, subtitle=subtitle) 80 | 81 | else: 82 | """Last case""" 83 | wf.add_item( 84 | "Complete", 85 | subtitle="Emoji searching is now ready to use", 86 | icon="images/Checkmark.png", 87 | ) 88 | 89 | 90 | def main(wf: Workflow3) -> None: 91 | """Define Main function.""" 92 | log.debug("--dbg:\tmain Function") 93 | 94 | try: 95 | count = int(os.environ["count"]) 96 | first_time = False 97 | except KeyError: 98 | count = 0 99 | first_time = True 100 | 101 | if is_running("emoji_init"): 102 | first_time = False 103 | 104 | if first_time: 105 | wf.rerun = 0.5 106 | wf.store_data("download_percent", 0) 107 | wf.store_data("phase", "downloading") 108 | wf.store_data("emoji_count", 0) 109 | wf.add_item("Starting background process") 110 | run_in_background( 111 | "emoji_init", ["/usr/bin/python3", wf.workflowfile("src/bg_downloader.py")] 112 | ) 113 | log.debug("Launching background task") 114 | else: 115 | build_wf_entry(wf) 116 | 117 | wf.setvar("count", count) 118 | 119 | wf.send_feedback() 120 | 121 | 122 | if __name__ == "__main__": 123 | wf = Workflow3() 124 | log = wf.logger 125 | log.info("Emoji Init Started") 126 | sys.exit(wf.run(main)) 127 | -------------------------------------------------------------------------------- /src/esearch.py: -------------------------------------------------------------------------------- 1 | """Emoji Search.""" 2 | from __future__ import annotations 3 | 4 | import os 5 | import sys 6 | from urllib.parse import quote 7 | 8 | from workflow import Workflow3 9 | 10 | base_path = os.path.dirname(os.path.realpath(__file__)) 11 | 12 | name_match = [] 13 | keyword_match = [] 14 | 15 | 16 | def main(wf: Workflow3) -> None: 17 | """Define Main function.""" 18 | if os.path.isfile(f"{base_path}/../emoji.tab"): 19 | pass 20 | else: 21 | wf.add_item( 22 | 'Emoji not initialized. Hit enter or type "init emoji".', 23 | "The init script requires you to be connected to the internet", 24 | arg="init emoji", 25 | valid=True, 26 | ) 27 | log.info("Not Initialized") 28 | wf.send_feedback() 29 | exit() 30 | 31 | try: 32 | query: str | list[str] = [str(arg) for arg in wf.args[0:]] 33 | log.info("Query=%s", query) 34 | except: 35 | query = "" 36 | 37 | with open(f"{base_path}/../emoji.tab") as f: 38 | for idx, line in enumerate(f, 1): 39 | split_list = line.strip().split("\t") 40 | if len(split_list) != 6: 41 | raise ValueError( 42 | "Line {}: '{}' has {} spaces, expected 1".format( 43 | idx, line.rstrip(), len(split_list) - 1 44 | ) 45 | ) 46 | else: 47 | img, name, code, raw_code, code_string, keywords = split_list 48 | 49 | in_keywords = True 50 | in_name = True 51 | 52 | for term in query: 53 | 54 | if term.startswith("-"): 55 | in_name &= term[1:] not in name.lower() 56 | in_keywords &= term[1:] not in keywords.lower() 57 | else: 58 | in_name &= term in name.lower() 59 | in_keywords &= term in keywords.lower() 60 | 61 | if in_name: 62 | name_match.append([img, name, raw_code, keywords]) 63 | elif in_keywords: 64 | keyword_match.append([img, name, raw_code, keywords]) 65 | 66 | imgbase = "file://" + quote(base_path) + "/../img/" 67 | 68 | if len(name_match + keyword_match) == 0: 69 | sad_face = "\U0001F622\U0000FE0F" 70 | wf.add_item( 71 | sad_face + " No Emoji found", 72 | "Please try again", 73 | icon="icon.png", 74 | ) 75 | else: 76 | if len(name_match + keyword_match) > 9: 77 | wf.add_item( 78 | str(len(name_match + keyword_match)) + " matches found", 79 | "To remove results use - in front of a term", 80 | icon="icon.png", 81 | valid=False, 82 | ) 83 | for array in name_match + keyword_match: 84 | img, title, raw_code, subtitle = array 85 | 86 | ql = imgbase + img 87 | 88 | item = wf.add_item( 89 | f"{title}", 90 | subtitle=subtitle.replace(" ", " "), 91 | icon="img/" + img, 92 | quicklookurl=ql, 93 | arg=f"{raw_code.encode('utf-8').decode('unicode_escape')}", # This is not raw code any more 94 | valid=True, 95 | ) 96 | 97 | p_string = raw_code 98 | pd_string = 'u"' + raw_code + '"' 99 | 100 | log.info(p_string) 101 | log.info(pd_string) 102 | 103 | item.add_modifier( 104 | "cmd", subtitle="Python String [" + p_string + "]", arg=p_string, valid=None 105 | ) 106 | item.add_modifier( 107 | "alt", 108 | subtitle="Unicode Value [" + code_string + "]", 109 | arg=code_string, 110 | valid=None, 111 | ) 112 | item.add_modifier( 113 | "ctrl", 114 | subtitle="Python String (decoded) [" + pd_string + "]", 115 | arg=pd_string, 116 | valid=None, 117 | ) 118 | 119 | wf.send_feedback() 120 | 121 | 122 | if __name__ == "__main__": 123 | # wf = Workflow3(update_settings={"github_slug": "jeeftor/EmojiTaco", "frequency": 7}) 124 | wf = Workflow3() 125 | log = wf.logger 126 | sys.exit(wf.run(main)) 127 | -------------------------------------------------------------------------------- /src/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeeftor/EmojiTaco/a58096cf1af2977efe6dbab8bb8fd7b58c6b922c/src/libs/__init__.py -------------------------------------------------------------------------------- /src/libs/bs4/diagnose.py: -------------------------------------------------------------------------------- 1 | """Diagnostic functions, mainly for use when doing tech support.""" 2 | 3 | # Use of this source code is governed by the MIT license. 4 | __license__ = "MIT" 5 | 6 | import cProfile 7 | from io import StringIO 8 | from html.parser import HTMLParser 9 | import bs4 10 | from bs4 import BeautifulSoup, __version__ 11 | from bs4.builder import builder_registry 12 | 13 | import os 14 | import pstats 15 | import random 16 | import tempfile 17 | import time 18 | import traceback 19 | import sys 20 | import cProfile 21 | 22 | def diagnose(data): 23 | """Diagnostic suite for isolating common problems. 24 | 25 | :param data: A string containing markup that needs to be explained. 26 | :return: None; diagnostics are printed to standard output. 27 | """ 28 | print(("Diagnostic running on Beautiful Soup %s" % __version__)) 29 | print(("Python version %s" % sys.version)) 30 | 31 | basic_parsers = ["html.parser", "html5lib", "lxml"] 32 | for name in basic_parsers: 33 | for builder in builder_registry.builders: 34 | if name in builder.features: 35 | break 36 | else: 37 | basic_parsers.remove(name) 38 | print(( 39 | "I noticed that %s is not installed. Installing it may help." % 40 | name)) 41 | 42 | if 'lxml' in basic_parsers: 43 | basic_parsers.append("lxml-xml") 44 | try: 45 | from lxml import etree 46 | print(("Found lxml version %s" % ".".join(map(str,etree.LXML_VERSION)))) 47 | except ImportError as e: 48 | print( 49 | "lxml is not installed or couldn't be imported.") 50 | 51 | 52 | if 'html5lib' in basic_parsers: 53 | try: 54 | import html5lib 55 | print(("Found html5lib version %s" % html5lib.__version__)) 56 | except ImportError as e: 57 | print( 58 | "html5lib is not installed or couldn't be imported.") 59 | 60 | if hasattr(data, 'read'): 61 | data = data.read() 62 | elif data.startswith("http:") or data.startswith("https:"): 63 | print(('"%s" looks like a URL. Beautiful Soup is not an HTTP client.' % data)) 64 | print("You need to use some other library to get the document behind the URL, and feed that document to Beautiful Soup.") 65 | return 66 | else: 67 | try: 68 | if os.path.exists(data): 69 | print(('"%s" looks like a filename. Reading data from the file.' % data)) 70 | with open(data) as fp: 71 | data = fp.read() 72 | except ValueError: 73 | # This can happen on some platforms when the 'filename' is 74 | # too long. Assume it's data and not a filename. 75 | pass 76 | print("") 77 | 78 | for parser in basic_parsers: 79 | print(("Trying to parse your markup with %s" % parser)) 80 | success = False 81 | try: 82 | soup = BeautifulSoup(data, features=parser) 83 | success = True 84 | except Exception as e: 85 | print(("%s could not parse the markup." % parser)) 86 | traceback.print_exc() 87 | if success: 88 | print(("Here's what %s did with the markup:" % parser)) 89 | print((soup.prettify())) 90 | 91 | print(("-" * 80)) 92 | 93 | def lxml_trace(data, html=True, **kwargs): 94 | """Print out the lxml events that occur during parsing. 95 | 96 | This lets you see how lxml parses a document when no Beautiful 97 | Soup code is running. You can use this to determine whether 98 | an lxml-specific problem is in Beautiful Soup's lxml tree builders 99 | or in lxml itself. 100 | 101 | :param data: Some markup. 102 | :param html: If True, markup will be parsed with lxml's HTML parser. 103 | if False, lxml's XML parser will be used. 104 | """ 105 | from lxml import etree 106 | for event, element in etree.iterparse(StringIO(data), html=html, **kwargs): 107 | print(("%s, %4s, %s" % (event, element.tag, element.text))) 108 | 109 | class AnnouncingParser(HTMLParser): 110 | """Subclass of HTMLParser that announces parse events, without doing 111 | anything else. 112 | 113 | You can use this to get a picture of how html.parser sees a given 114 | document. The easiest way to do this is to call `htmlparser_trace`. 115 | """ 116 | 117 | def _p(self, s): 118 | print(s) 119 | 120 | def handle_starttag(self, name, attrs): 121 | self._p("%s START" % name) 122 | 123 | def handle_endtag(self, name): 124 | self._p("%s END" % name) 125 | 126 | def handle_data(self, data): 127 | self._p("%s DATA" % data) 128 | 129 | def handle_charref(self, name): 130 | self._p("%s CHARREF" % name) 131 | 132 | def handle_entityref(self, name): 133 | self._p("%s ENTITYREF" % name) 134 | 135 | def handle_comment(self, data): 136 | self._p("%s COMMENT" % data) 137 | 138 | def handle_decl(self, data): 139 | self._p("%s DECL" % data) 140 | 141 | def unknown_decl(self, data): 142 | self._p("%s UNKNOWN-DECL" % data) 143 | 144 | def handle_pi(self, data): 145 | self._p("%s PI" % data) 146 | 147 | def htmlparser_trace(data): 148 | """Print out the HTMLParser events that occur during parsing. 149 | 150 | This lets you see how HTMLParser parses a document when no 151 | Beautiful Soup code is running. 152 | 153 | :param data: Some markup. 154 | """ 155 | parser = AnnouncingParser() 156 | parser.feed(data) 157 | 158 | _vowels = "aeiou" 159 | _consonants = "bcdfghjklmnpqrstvwxyz" 160 | 161 | def rword(length=5): 162 | "Generate a random word-like string." 163 | s = '' 164 | for i in range(length): 165 | if i % 2 == 0: 166 | t = _consonants 167 | else: 168 | t = _vowels 169 | s += random.choice(t) 170 | return s 171 | 172 | def rsentence(length=4): 173 | "Generate a random sentence-like string." 174 | return " ".join(rword(random.randint(4,9)) for i in range(length)) 175 | 176 | def rdoc(num_elements=1000): 177 | """Randomly generate an invalid HTML document.""" 178 | tag_names = ['p', 'div', 'span', 'i', 'b', 'script', 'table'] 179 | elements = [] 180 | for i in range(num_elements): 181 | choice = random.randint(0,3) 182 | if choice == 0: 183 | # New tag. 184 | tag_name = random.choice(tag_names) 185 | elements.append("<%s>" % tag_name) 186 | elif choice == 1: 187 | elements.append(rsentence(random.randint(1,4))) 188 | elif choice == 2: 189 | # Close a tag. 190 | tag_name = random.choice(tag_names) 191 | elements.append("" % tag_name) 192 | return "" + "\n".join(elements) + "" 193 | 194 | def benchmark_parsers(num_elements=100000): 195 | """Very basic head-to-head performance benchmark.""" 196 | print(("Comparative parser benchmark on Beautiful Soup %s" % __version__)) 197 | data = rdoc(num_elements) 198 | print(("Generated a large invalid HTML document (%d bytes)." % len(data))) 199 | 200 | for parser in ["lxml", ["lxml", "html"], "html5lib", "html.parser"]: 201 | success = False 202 | try: 203 | a = time.time() 204 | soup = BeautifulSoup(data, parser) 205 | b = time.time() 206 | success = True 207 | except Exception as e: 208 | print(("%s could not parse the markup." % parser)) 209 | traceback.print_exc() 210 | if success: 211 | print(("BS4+%s parsed the markup in %.2fs." % (parser, b-a))) 212 | 213 | from lxml import etree 214 | a = time.time() 215 | etree.HTML(data) 216 | b = time.time() 217 | print(("Raw lxml parsed the markup in %.2fs." % (b-a))) 218 | 219 | import html5lib 220 | parser = html5lib.HTMLParser() 221 | a = time.time() 222 | parser.parse(data) 223 | b = time.time() 224 | print(("Raw html5lib parsed the markup in %.2fs." % (b-a))) 225 | 226 | def profile(num_elements=100000, parser="lxml"): 227 | """Use Python's profiler on a randomly generated document.""" 228 | filehandle = tempfile.NamedTemporaryFile() 229 | filename = filehandle.name 230 | 231 | data = rdoc(num_elements) 232 | vars = dict(bs4=bs4, data=data, parser=parser) 233 | cProfile.runctx('bs4.BeautifulSoup(data, parser)' , vars, vars, filename) 234 | 235 | stats = pstats.Stats(filename) 236 | # stats.strip_dirs() 237 | stats.sort_stats("cumulative") 238 | stats.print_stats('_html5lib|bs4', 50) 239 | 240 | # If this file is run as a script, standard input is diagnosed. 241 | if __name__ == '__main__': 242 | diagnose(sys.stdin.read()) 243 | -------------------------------------------------------------------------------- /src/libs/bs4/formatter.py: -------------------------------------------------------------------------------- 1 | from bs4.dammit import EntitySubstitution 2 | 3 | class Formatter(EntitySubstitution): 4 | """Describes a strategy to use when outputting a parse tree to a string. 5 | 6 | Some parts of this strategy come from the distinction between 7 | HTML4, HTML5, and XML. Others are configurable by the user. 8 | 9 | Formatters are passed in as the `formatter` argument to methods 10 | like `PageElement.encode`. Most people won't need to think about 11 | formatters, and most people who need to think about them can pass 12 | in one of these predefined strings as `formatter` rather than 13 | making a new Formatter object: 14 | 15 | For HTML documents: 16 | * 'html' - HTML entity substitution for generic HTML documents. (default) 17 | * 'html5' - HTML entity substitution for HTML5 documents, as 18 | well as some optimizations in the way tags are rendered. 19 | * 'minimal' - Only make the substitutions necessary to guarantee 20 | valid HTML. 21 | * None - Do not perform any substitution. This will be faster 22 | but may result in invalid markup. 23 | 24 | For XML documents: 25 | * 'html' - Entity substitution for XHTML documents. 26 | * 'minimal' - Only make the substitutions necessary to guarantee 27 | valid XML. (default) 28 | * None - Do not perform any substitution. This will be faster 29 | but may result in invalid markup. 30 | """ 31 | # Registries of XML and HTML formatters. 32 | XML_FORMATTERS = {} 33 | HTML_FORMATTERS = {} 34 | 35 | HTML = 'html' 36 | XML = 'xml' 37 | 38 | HTML_DEFAULTS = dict( 39 | cdata_containing_tags=set(["script", "style"]), 40 | ) 41 | 42 | def _default(self, language, value, kwarg): 43 | if value is not None: 44 | return value 45 | if language == self.XML: 46 | return set() 47 | return self.HTML_DEFAULTS[kwarg] 48 | 49 | def __init__( 50 | self, language=None, entity_substitution=None, 51 | void_element_close_prefix='/', cdata_containing_tags=None, 52 | empty_attributes_are_booleans=False, 53 | ): 54 | """Constructor. 55 | 56 | :param language: This should be Formatter.XML if you are formatting 57 | XML markup and Formatter.HTML if you are formatting HTML markup. 58 | 59 | :param entity_substitution: A function to call to replace special 60 | characters with XML/HTML entities. For examples, see 61 | bs4.dammit.EntitySubstitution.substitute_html and substitute_xml. 62 | :param void_element_close_prefix: By default, void elements 63 | are represented as (XML rules) rather than 64 | (HTML rules). To get , pass in the empty string. 65 | :param cdata_containing_tags: The list of tags that are defined 66 | as containing CDATA in this dialect. For example, in HTML, 67 |