├── .gitignore ├── LICENSE ├── README.md ├── conftest.py ├── docs └── img │ └── screenshot-editor.png ├── manage.py ├── pyproject.toml ├── tests ├── __init__.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── settings.py ├── test_blocks.py └── urls.py └── wagtailcodeblock ├── __init__.py ├── blocks.py ├── settings.py ├── static └── wagtailcodeblock │ ├── css │ ├── wagtail-code-block.css │ └── wagtail-code-block.min.css │ └── js │ └── wagtailcodeblock.js ├── templates └── wagtailcodeblock │ ├── code_block.html │ ├── code_block_form.html │ └── raw_code.html ├── templatetags ├── __init__.py └── wagtailcodeblock_tags.py └── wagtail_hooks.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build, tests, and venvs 2 | build/* 3 | dist/* 4 | htmlcov/* 5 | venv/* 6 | wagtailcodeblock.egg-info/* 7 | test_db.sqlite3 8 | wagtailcodeblock/_version.py 9 | 10 | # manage.py, always default to dev settings 11 | manage.py 12 | 13 | # Django media files 14 | media/* 15 | 16 | # Python bytecode: 17 | *.py[co] 18 | 19 | # Packaging files: 20 | *.egg* 21 | 22 | # Sphinx docs: 23 | build 24 | 25 | # SQLite3 database files: 26 | *.db 27 | 28 | # Logs: 29 | *.log 30 | 31 | # Eclipse 32 | .project 33 | 34 | # Linux Editors 35 | *~ 36 | \#*\# 37 | /.emacs.desktop 38 | /.emacs.desktop.lock 39 | .elc 40 | auto-save-list 41 | tramp 42 | .\#* 43 | *.swp 44 | *.swo 45 | 46 | # Mac 47 | .DS_Store 48 | ._* 49 | 50 | # Windows 51 | Thumbs.db 52 | Desktop.ini 53 | 54 | # Dev tools 55 | .idea 56 | .vagrant 57 | 58 | # Ignore local configurations 59 | wrds/settings/local.py 60 | db.sqlite3 61 | 62 | # Node modules 63 | node_modules/ 64 | 65 | # Bower components 66 | bower_components/ 67 | 68 | # Coverage 69 | htmlcov/ 70 | .coverage 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Timothy Allen and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Timothy Allen, The Wharton School, nor the names of 15 | its contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wagtail Code Block 2 | 3 | Wagtail Code Block is a syntax highlighter block for source code for the Wagtail CMS. It features real-time highlighting in the Wagtail editor, the front end, line numbering, and support for PrismJS themes. 4 | 5 | It uses the [PrismJS](http://prismjs.com/) library both in Wagtail Admin and the website. 6 | 7 | ## Example Usage 8 | 9 | First, add `wagtailcodeblock` to your `INSTALLED_APPS` in Django's settings. Here's a bare bones example: 10 | 11 | ```python 12 | from wagtail.blocks import TextBlock 13 | from wagtail.fields import StreamField 14 | from wagtail.models import Page 15 | from wagtail.admin.panels import FieldPanel 16 | 17 | from wagtailcodeblock.blocks import CodeBlock 18 | 19 | 20 | class HomePage(Page): 21 | body = StreamField([ 22 | ("heading", TextBlock()), 23 | ("code", CodeBlock(label='Code')), 24 | ]) 25 | 26 | content_panels = Page.content_panels + [ 27 | FieldPanel("body"), 28 | ] 29 | ``` 30 | 31 | You can also force it to use a single language or set a default language by providing a language code which must be included in your `WAGTAIL_CODE_BLOCK_LANGUAGES` setting: 32 | 33 | ```python 34 | bash_code = CodeBlock(label='Bash Code', language='bash') 35 | any_code = CodeBlock(label='Any code', default_language='python') 36 | ``` 37 | 38 | ## Screenshot of the CMS Editor Interface 39 | 40 | ![Admin in Action](https://raw.githubusercontent.com/FlipperPA/wagtailcodeblock/main/docs/img/screenshot-editor.png) 41 | 42 | ## Installation & Setup 43 | 44 | To install Wagtail Code Block run: 45 | 46 | ```bash 47 | # Wagtail 4.0 and greater 48 | pip install wagtailcodeblock 49 | 50 | # Wagtail 3.x 51 | pip install wagtailcodeblock==1.28.0.0 52 | 53 | # Wagtail 2.x 54 | pip install wagtailcodeblock==1.25.0.2 55 | ``` 56 | 57 | And add `wagtailcodeblock` to your `INSTALLED_APPS` setting: 58 | 59 | ```python 60 | INSTALLED_APPS = [ 61 | ... 62 | 'wagtailcodeblock', 63 | ... 64 | ] 65 | ``` 66 | 67 | ## Django Settings 68 | 69 | ### Line Numbers 70 | 71 | Line numbers are enabled by default, but can be disabled in Django's settings: 72 | 73 | ```python 74 | WAGTAIL_CODE_BLOCK_LINE_NUMBERS = False 75 | ``` 76 | 77 | ### Copy to clipboard 78 | 79 | Copy to clipboard is enabled by default, but can be disabled in Django's settings: 80 | 81 | ```python 82 | WAGTAIL_CODE_BLOCK_COPY_TO_CLIPBOARD = False 83 | ``` 84 | 85 | ### Themes 86 | 87 | Wagtail Code Block defaults to the PrismJS "Coy" theme, which looks good with Wagtail's CMS editor design. You can choose a different theme by configuring `WAGTAIL_CODE_BLOCK_THEME` in your Django settings. PrismJS provides several themes: 88 | 89 | * **None**: Default 90 | * **'coy'**: Coy 91 | * **'dark'**: Dark 92 | * **'funky'**: Funky 93 | * **'okaidia'**: Okaidia 94 | * **'solarizedlight'**: Solarized Light 95 | * **'twilight'**: Twilight 96 | 97 | For example, in you want to use the Solarized Light theme: `WAGTAIL_CODE_BLOCK_THEME = 'solarizedlight'` 98 | If you want to use the Default theme: `WAGTAIL_CODE_BLOCK_THEME = None` 99 | 100 | ### Languages Available 101 | 102 | You can customize the languages available by configuring `WAGTAIL_CODE_BLOCK_LANGUAGES` in your Django settings. By default, it will be set with these languages, since most users are in the Python web development community: 103 | 104 | ```python 105 | WAGTAIL_CODE_BLOCK_LANGUAGES = ( 106 | ('bash', 'Bash/Shell'), 107 | ('css', 'CSS'), 108 | ('diff', 'diff'), 109 | ('html', 'HTML'), 110 | ('javascript', 'Javascript'), 111 | ('json', 'JSON'), 112 | ('python', 'Python'), 113 | ('scss', 'SCSS'), 114 | ('yaml', 'YAML'), 115 | ) 116 | ``` 117 | 118 | Each language in this setting is a tuple of the PrismJS code and a descriptive label. If you want use all available languages, here is a list: 119 | 120 | ```python 121 | WAGTAIL_CODE_BLOCK_LANGUAGES = ( 122 | ('abap', 'ABAP'), 123 | ('abnf', 'Augmented Backus–Naur form'), 124 | ('actionscript', 'ActionScript'), 125 | ('ada', 'Ada'), 126 | ('antlr4', 'ANTLR4'), 127 | ('apacheconf', 'Apache Configuration'), 128 | ('apl', 'APL'), 129 | ('applescript', 'AppleScript'), 130 | ('aql', 'AQL'), 131 | ('arduino', 'Arduino'), 132 | ('arff', 'ARFF'), 133 | ('asciidoc', 'AsciiDoc'), 134 | ('asm6502', '6502 Assembly'), 135 | ('aspnet', 'ASP.NET (C#)'), 136 | ('autohotkey', 'AutoHotkey'), 137 | ('autoit', 'AutoIt'), 138 | ('bash', 'Bash + Shell'), 139 | ('basic', 'BASIC'), 140 | ('batch', 'Batch'), 141 | ('bison', 'Bison'), 142 | ('bnf', 'Backus–Naur form + Routing Backus–Naur form'), 143 | ('brainfuck', 'Brainfuck'), 144 | ('bro', 'Bro'), 145 | ('c', 'C'), 146 | ('clike', 'C-like'), 147 | ('cmake', 'CMake'), 148 | ('csharp', 'C#'), 149 | ('cpp', 'C++'), 150 | ('cil', 'CIL'), 151 | ('coffeescript', 'CoffeeScript'), 152 | ('clojure', 'Clojure'), 153 | ('crystal', 'Crystal'), 154 | ('csp', 'Content-Security-Policy'), 155 | ('css', 'CSS'), 156 | ('css-extras', 'CSS Extras'), 157 | ('d', 'D'), 158 | ('dart', 'Dart'), 159 | ('diff', 'Diff'), 160 | ('django', 'Django/Jinja2'), 161 | ('dns-zone-file', 'DNS Zone File'), 162 | ('docker', 'Docker'), 163 | ('ebnf', 'Extended Backus–Naur form'), 164 | ('eiffel', 'Eiffel'), 165 | ('ejs', 'EJS'), 166 | ('elixir', 'Elixir'), 167 | ('elm', 'Elm'), 168 | ('erb', 'ERB'), 169 | ('erlang', 'Erlang'), 170 | ('etlua', 'Embedded LUA Templating'), 171 | ('fsharp', 'F#'), 172 | ('flow', 'Flow'), 173 | ('fortran', 'Fortran'), 174 | ('ftl', 'Freemarker Template Language'), 175 | ('gcode', 'G-code'), 176 | ('gdscript', 'GDScript'), 177 | ('gedcom', 'GEDCOM'), 178 | ('gherkin', 'Gherkin'), 179 | ('git', 'Git'), 180 | ('glsl', 'GLSL'), 181 | ('gml', 'GameMaker Language'), 182 | ('go', 'Go'), 183 | ('graphql', 'GraphQL'), 184 | ('groovy', 'Groovy'), 185 | ('haml', 'Haml'), 186 | ('handlebars', 'Handlebars'), 187 | ('haskell', 'Haskell'), 188 | ('haxe', 'Haxe'), 189 | ('hcl', 'HCL'), 190 | ('http', 'HTTP'), 191 | ('hpkp', 'HTTP Public-Key-Pins'), 192 | ('hsts', 'HTTP Strict-Transport-Security'), 193 | ('ichigojam', 'IchigoJam'), 194 | ('icon', 'Icon'), 195 | ('inform7', 'Inform 7'), 196 | ('ini', 'Ini'), 197 | ('io', 'Io'), 198 | ('j', 'J'), 199 | ('java', 'Java'), 200 | ('javadoc', 'JavaDoc'), 201 | ('javadoclike', 'JavaDoc-like'), 202 | ('javascript', 'JavaScript'), 203 | ('javastacktrace', 'Java stack trace'), 204 | ('jolie', 'Jolie'), 205 | ('jq', 'JQ'), 206 | ('jsdoc', 'JSDoc'), 207 | ('js-extras', 'JS Extras'), 208 | ('js-templates', 'JS Templates'), 209 | ('json', 'JSON'), 210 | ('jsonp', 'JSONP'), 211 | ('json5', 'JSON5'), 212 | ('julia', 'Julia'), 213 | ('keyman', 'Keyman'), 214 | ('kotlin', 'Kotlin'), 215 | ('latex', 'LaTeX'), 216 | ('less', 'Less'), 217 | ('lilypond', 'Lilypond'), 218 | ('liquid', 'Liquid'), 219 | ('lisp', 'Lisp'), 220 | ('livescript', 'LiveScript'), 221 | ('lolcode', 'LOLCODE'), 222 | ('lua', 'Lua'), 223 | ('makefile', 'Makefile'), 224 | ('markdown', 'Markdown'), 225 | ('markup', 'Markup + HTML + XML + SVG + MathML'), 226 | ('markup-templating', 'Markup templating'), 227 | ('matlab', 'MATLAB'), 228 | ('mel', 'MEL'), 229 | ('mizar', 'Mizar'), 230 | ('monkey', 'Monkey'), 231 | ('n1ql', 'N1QL'), 232 | ('n4js', 'N4JS'), 233 | ('nand2tetris-hdl', 'Nand To Tetris HDL'), 234 | ('nasm', 'NASM'), 235 | ('nginx', 'nginx'), 236 | ('nim', 'Nim'), 237 | ('nix', 'Nix'), 238 | ('nsis', 'NSIS'), 239 | ('objectivec', 'Objective-C'), 240 | ('ocaml', 'OCaml'), 241 | ('opencl', 'OpenCL'), 242 | ('oz', 'Oz'), 243 | ('parigp', 'PARI/GP'), 244 | ('parser', 'Parser'), 245 | ('pascal', 'Pascal + Object Pascal'), 246 | ('pascaligo', 'Pascaligo'), 247 | ('pcaxis', 'PC Axis'), 248 | ('perl', 'Perl'), 249 | ('php', 'PHP'), 250 | ('phpdoc', 'PHPDoc'), 251 | ('php-extras', 'PHP Extras'), 252 | ('plsql', 'PL/SQL'), 253 | ('powershell', 'PowerShell'), 254 | ('processing', 'Processing'), 255 | ('prolog', 'Prolog'), 256 | ('properties', '.properties'), 257 | ('protobuf', 'Protocol Buffers'), 258 | ('pug', 'Pug'), 259 | ('puppet', 'Puppet'), 260 | ('pure', 'Pure'), 261 | ('python', 'Python'), 262 | ('q', 'Q (kdb+ database)'), 263 | ('qore', 'Qore'), 264 | ('r', 'R'), 265 | ('jsx', 'React JSX'), 266 | ('tsx', 'React TSX'), 267 | ('renpy', 'Ren\'py'), 268 | ('reason', 'Reason'), 269 | ('regex', 'Regex'), 270 | ('rest', 'reST (reStructuredText)'), 271 | ('rip', 'Rip'), 272 | ('roboconf', 'Roboconf'), 273 | ('robot-framework', 'Robot Framework'), 274 | ('ruby', 'Ruby'), 275 | ('rust', 'Rust'), 276 | ('sas', 'SAS'), 277 | ('sass', 'Sass (Sass)'), 278 | ('scss', 'Sass (Scss)'), 279 | ('scala', 'Scala'), 280 | ('scheme', 'Scheme'), 281 | ('shell-session', 'Shell Session'), 282 | ('smalltalk', 'Smalltalk'), 283 | ('smarty', 'Smarty'), 284 | ('solidity', 'Solidity (Ethereum)'), 285 | ('sparql', 'SPARQL'), 286 | ('splunk-spl', 'Splunk SPL'), 287 | ('sqf', 'SQF: Status Quo Function (Arma 3)'), 288 | ('sql', 'SQL'), 289 | ('soy', 'Soy (Closure Template)'), 290 | ('stylus', 'Stylus'), 291 | ('swift', 'Swift'), 292 | ('tap', 'TAP'), 293 | ('tcl', 'Tcl'), 294 | ('textile', 'Textile'), 295 | ('toml', 'TOML'), 296 | ('tt2', 'Template Toolkit 2'), 297 | ('twig', 'Twig'), 298 | ('typescript', 'TypeScript'), 299 | ('t4-cs', 'T4 Text Templates (C#)'), 300 | ('t4-vb', 'T4 Text Templates (VB)'), 301 | ('t4-templating', 'T4 templating'), 302 | ('vala', 'Vala'), 303 | ('vbnet', 'VB.Net'), 304 | ('velocity', 'Velocity'), 305 | ('verilog', 'Verilog'), 306 | ('vhdl', 'VHDL'), 307 | ('vim', 'vim'), 308 | ('visual-basic', 'Visual Basic'), 309 | ('wasm', 'WebAssembly'), 310 | ('wiki', 'Wiki markup'), 311 | ('xeora', 'Xeora + XeoraCube'), 312 | ('xojo', 'Xojo (REALbasic)'), 313 | ('xquery', 'XQuery'), 314 | ('yaml', 'YAML'), 315 | ('zig', 'Zig'), 316 | ) 317 | ``` 318 | 319 | # What's With the Versioning? 320 | 321 | Our version numbers are based on the underlying version of PrismJS we use. For example, if we are using PrismJS `1.28.0`, our versions will be named `1.28.0.X`. 322 | 323 | # Running the Test Suite 324 | 325 | Clone the repository, create a `venv`, `pip install -e .[dev]` and run `pytest`. 326 | 327 | # Release Notes & Contributors 328 | 329 | * Thank you to our [wonderful contributors](https://github.com/FlipperPA/wagtailcodeblock/graphs/contributors)! 330 | * Release notes are [available on GitHub](https://github.com/FlipperPA/wagtailcodeblock/releases). 331 | 332 | # Project Maintainers 333 | 334 | * Timothy Allen (https://github.com/FlipperPA) 335 | * Milton Lenis (https://github.com/MiltonLn) 336 | 337 | This package was created by the staff of [Wharton Research Data Services](https://wrds.wharton.upenn.edu/). We are thrilled that [The Wharton School](https://www.wharton.upenn.edu/) allows us a certain amount of time to contribute to open-source projects. We add features as they are necessary for our projects, and try to keep up with Issues and Pull Requests as best we can. Due to constraints of time (our full time jobs!), Feature Requests without a Pull Request may not be implemented, but we are always open to new ideas and grateful for contributions and our users. 338 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | 3 | from django.contrib.contenttypes.models import ContentType 4 | 5 | import pytest 6 | from wagtail.models import Page, Locale, Site 7 | 8 | from tests.models import CodeBlockPage 9 | 10 | 11 | @pytest.fixture 12 | def test_page(db): 13 | """ 14 | Create a root page in the same way Wagtail does in migrations. See: 15 | https://github.com/wagtail/wagtail/blob/main/wagtail/core/migrations/0002_initial_data.py#L12 # noqa 16 | 17 | Then create the test page. 18 | """ 19 | page_content_type, created = ContentType.objects.get_or_create( 20 | model="page", app_label="wagtailcore" 21 | ) 22 | 23 | root_page, created = Page.objects.get_or_create( 24 | title="Root", 25 | slug="root", 26 | content_type=page_content_type, 27 | path="0001", 28 | depth=1, 29 | numchild=1, 30 | url_path="/", 31 | ) 32 | 33 | test_page, created = CodeBlockPage.objects.get_or_create( 34 | # Required Wagtail Page fields 35 | title="TEST Wagtail Code Block Page", 36 | slug="wagtail-code-block", 37 | content_type=page_content_type, 38 | path="00010002", 39 | depth=2, 40 | numchild=0, 41 | url_path="/wagtail-code-block/", 42 | # Wagtail Code Block test fields 43 | body=dumps([{ 44 | "type": "code", 45 | "value": { 46 | "language": "python", 47 | "code": "print([x for x in range(1, 5)])", 48 | }, 49 | }]), 50 | ) 51 | 52 | return test_page 53 | -------------------------------------------------------------------------------- /docs/img/screenshot-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/wagtailcodeblock/11aaf676b236c22bceac8c9cf778a1f55003ccb9/docs/img/screenshot-editor.png -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "wagtailcodeblock" 3 | authors = [{name = "Tim Allen", email = "tallen@wharton.upenn.edu"},] 4 | description = "Wagtail Code Block provides PrismJS syntax highlighting in Wagtail." 5 | dynamic = ["version"] 6 | readme = "README.md" 7 | requires-python = ">=3.7" 8 | keywords = ["wagtail", "cms", "contact", "syntax", "code", "highlighting", "highlighter"] 9 | license = {text = "BSD-3-Clause"} 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Environment :: Web Environment", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: BSD License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.8", 18 | "Programming Language :: Python :: 3.9", 19 | "Programming Language :: Python :: 3.10", 20 | "Programming Language :: Python :: 3.11", 21 | "Programming Language :: Python :: 3.12", 22 | "Framework :: Django", 23 | "Framework :: Wagtail", 24 | "Framework :: Wagtail :: 3", 25 | "Framework :: Wagtail :: 4", 26 | "Framework :: Wagtail :: 5", 27 | "Framework :: Wagtail :: 6", 28 | "Topic :: Internet :: WWW/HTTP", 29 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 30 | ] 31 | dependencies = [ 32 | "wagtail>=4", 33 | ] 34 | 35 | [project.optional-dependencies] 36 | dev = [ 37 | "django-coverage-plugin", 38 | "ipython", 39 | "ruff", 40 | "pytest-coverage", 41 | "pytest-django", 42 | ] 43 | 44 | [project.urls] 45 | "Homepage" = "https://github.com/FlipperPA/wagtailcodeblock" 46 | "Repository" = "https://github.com/FlipperPA/wagtailcodeblock" 47 | "Documentation" = "https://github.com/FlipperPA/wagtailcodeblock" 48 | 49 | [build-system] 50 | requires = ["setuptools>=67", "setuptools_scm>=7", "wheel"] 51 | build-backend = "setuptools.build_meta" 52 | 53 | [tool.setuptools_scm] 54 | write_to = "wagtailcodeblock/_version.py" 55 | 56 | [tool.pytest.ini_options] 57 | addopts = "--cov --cov-report=html" 58 | python_files = "tests.py test_*.py" 59 | DJANGO_SETTINGS_MODULE = "tests.settings" 60 | 61 | [tool.coverage.run] 62 | plugins = ["django_coverage_plugin"] 63 | include = ["wagtailcodeblock/*"] 64 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/wagtailcodeblock/11aaf676b236c22bceac8c9cf778a1f55003ccb9/tests/__init__.py -------------------------------------------------------------------------------- /tests/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-10-23 15:29 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import wagtail.blocks 6 | import wagtail.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ("wagtailcore", "0041_group_collection_permissions_verbose_name_plural"), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name="CodeBlockPage", 20 | fields=[ 21 | ( 22 | "page_ptr", 23 | models.OneToOneField( 24 | auto_created=True, 25 | on_delete=django.db.models.deletion.CASCADE, 26 | parent_link=True, 27 | primary_key=True, 28 | serialize=False, 29 | to="wagtailcore.Page", 30 | ), 31 | ), 32 | ( 33 | "body", 34 | wagtail.fields.StreamField( 35 | [ 36 | ( 37 | "code", 38 | wagtail.blocks.StructBlock( 39 | [ 40 | ( 41 | "language", 42 | wagtail.blocks.ChoiceBlock( 43 | choices=[ 44 | ("bash", "Bash/Shell"), 45 | ("css", "CSS"), 46 | ("diff", "diff"), 47 | ("html", "HTML"), 48 | ("javascript", "Javascript"), 49 | ("json", "JSON"), 50 | ("python", "Python"), 51 | ("scss", "SCSS"), 52 | ("yaml", "YAML"), 53 | ], 54 | help_text="Coding language", 55 | identifier="language", 56 | label="Language", 57 | ), 58 | ), 59 | ( 60 | "code", 61 | wagtail.blocks.TextBlock( 62 | identifier="code", label="Code" 63 | ), 64 | ), 65 | ] 66 | ), 67 | ) 68 | ], 69 | blank=True, 70 | ), 71 | ), 72 | ], 73 | options={"abstract": False,}, 74 | bases=("wagtailcore.page",), 75 | ), 76 | ] 77 | -------------------------------------------------------------------------------- /tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/wagtailcodeblock/11aaf676b236c22bceac8c9cf778a1f55003ccb9/tests/migrations/__init__.py -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | from wagtail.admin.panels import FieldPanel 2 | from wagtail.blocks import StreamBlock 3 | from wagtail.fields import StreamField 4 | from wagtail.models import Page 5 | from wagtailcodeblock.blocks import CodeBlock 6 | 7 | 8 | class CodeStreamBlock(StreamBlock): 9 | """ 10 | Test StreamBlock with a CodeBlock. 11 | """ 12 | 13 | code = CodeBlock() 14 | 15 | 16 | class CodeBlockPage(Page): 17 | """ 18 | Test Page with a code block in body. 19 | """ 20 | body = StreamField([ 21 | ('code', CodeBlock()), 22 | ], use_json_field=True) 23 | 24 | content_panels = Page.content_panels + [ 25 | FieldPanel("body"), 26 | ] 27 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | WAGTAILADMIN_BASE_URL = "https://example.com" 4 | ALLOWED_HOSTS = ["*"] 5 | SECRET_KEY = "tests" 6 | DEBUG = True 7 | USE_TZ = True 8 | 9 | TEMPLATES = [ 10 | { 11 | "BACKEND": "django.template.backends.django.DjangoTemplates", 12 | "APP_DIRS": True, 13 | "OPTIONS": { 14 | "context_processors": [ 15 | "django.template.context_processors.debug", 16 | "django.template.context_processors.request", 17 | "django.contrib.auth.context_processors.auth", 18 | "django.contrib.messages.context_processors.messages", 19 | ], 20 | "debug": True, 21 | }, 22 | } 23 | ] 24 | 25 | INSTALLED_APPS = settings.INSTALLED_APPS + [ 26 | "django.contrib.auth", 27 | "django.contrib.contenttypes", 28 | "django.contrib.messages", 29 | "django.contrib.sessions", 30 | "django.contrib.staticfiles", 31 | "wagtail", 32 | "wagtail.admin", 33 | "wagtail.documents", 34 | "tests", 35 | "wagtail.images", 36 | "wagtail.users", 37 | "wagtailcodeblock", 38 | "modelcluster", 39 | "taggit", 40 | ] 41 | 42 | MIDDLEWARE = settings.MIDDLEWARE + [ 43 | "django.middleware.security.SecurityMiddleware", 44 | "django.contrib.sessions.middleware.SessionMiddleware", 45 | "django.middleware.common.CommonMiddleware", 46 | "django.middleware.csrf.CsrfViewMiddleware", 47 | "django.contrib.auth.middleware.AuthenticationMiddleware", 48 | "django.contrib.messages.middleware.MessageMiddleware", 49 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 50 | ] 51 | 52 | ROOT_URLCONF = "tests.urls" 53 | 54 | DATABASES = { 55 | "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "test_db.sqlite3"} 56 | } 57 | 58 | STATIC_URL = "/static/" 59 | 60 | WAGTAIL_SITE_NAME = "Test Site" 61 | -------------------------------------------------------------------------------- /tests/test_blocks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_create_page(test_page): 6 | """ 7 | Tests creating a page with a Code Block. 8 | """ 9 | assert ( 10 | 'print([x for x in range(1, 5)])' 11 | in test_page.body.render_as_block() 12 | ) 13 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, re_path 2 | 3 | from wagtail.admin import urls as wagtailadmin_urls 4 | from wagtail.documents import urls as wagtaildocs_urls 5 | from wagtail import urls as wagtail_urls 6 | 7 | urlpatterns = [ 8 | re_path(r"^cms/", include(wagtailadmin_urls)), 9 | re_path(r"^documents/", include(wagtaildocs_urls)), 10 | re_path(r"", include(wagtail_urls)), 11 | ] 12 | -------------------------------------------------------------------------------- /wagtailcodeblock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/wagtailcodeblock/11aaf676b236c22bceac8c9cf778a1f55003ccb9/wagtailcodeblock/__init__.py -------------------------------------------------------------------------------- /wagtailcodeblock/blocks.py: -------------------------------------------------------------------------------- 1 | import wagtail 2 | 3 | from django.forms import Media 4 | from django.utils.functional import cached_property 5 | from django.utils.translation import gettext_lazy as _ 6 | 7 | from wagtail.blocks import ( 8 | StructBlock, 9 | TextBlock, 10 | ChoiceBlock, 11 | ) 12 | from wagtail.blocks.struct_block import StructBlockAdapter 13 | from wagtail.telepath import register 14 | 15 | from .settings import get_language_choices 16 | 17 | 18 | class CodeBlock(StructBlock): 19 | """ 20 | A Wagtail StreamField block for code syntax highlighting using PrismJS. 21 | """ 22 | 23 | def __init__(self, local_blocks=None, **kwargs): 24 | # Languages included in PrismJS core 25 | # Review: https://github.com/PrismJS/prism/blob/gh-pages/prism.js#L602 26 | self.INCLUDED_LANGUAGES = ( 27 | ("html", "HTML"), 28 | ("mathml", "MathML"), 29 | ("svg", "SVG"), 30 | ("xml", "XML"), 31 | ) 32 | 33 | if local_blocks is None: 34 | local_blocks = [] 35 | else: 36 | local_blocks = local_blocks.copy() 37 | 38 | language_choices, language_default = self.get_language_choice_list(**kwargs) 39 | 40 | local_blocks.extend( 41 | [ 42 | ( 43 | "language", 44 | ChoiceBlock( 45 | choices=language_choices, 46 | help_text=_("Coding language"), 47 | label=_("Language"), 48 | default=language_default, 49 | identifier="language", 50 | ), 51 | ), 52 | ("code", TextBlock(label=_("Code"), identifier="code")), 53 | ] 54 | ) 55 | 56 | super().__init__(local_blocks, **kwargs) 57 | 58 | def get_language_choice_list(self, **kwargs): 59 | # Get default languages 60 | WCB_LANGUAGES = get_language_choices() 61 | # If a language is passed in as part of a code block, use it. 62 | language = kwargs.get("language", False) 63 | 64 | total_language_choices = WCB_LANGUAGES + self.INCLUDED_LANGUAGES 65 | 66 | if language in [lang[0] for lang in total_language_choices]: 67 | for language_choice in total_language_choices: 68 | if language_choice[0] == language: 69 | language_choices = (language_choice,) 70 | language_default = language_choice[0] 71 | else: 72 | language_choices = WCB_LANGUAGES 73 | language_default = kwargs.get("default_language") 74 | 75 | return language_choices, language_default 76 | 77 | class Meta: 78 | icon = "code" 79 | template = "wagtailcodeblock/code_block.html" 80 | form_classname = "code-block struct-block" 81 | form_template = "wagtailcodeblock/code_block_form.html" 82 | 83 | 84 | class CodeBlockAdapter(StructBlockAdapter): 85 | js_constructor = "wagtailcodeblock.blocks.CodeBlock" 86 | 87 | @cached_property 88 | def media(self): 89 | structblock_media = super().media 90 | return Media( 91 | js=structblock_media._js + ["wagtailcodeblock/js/wagtailcodeblock.js"], 92 | css=structblock_media._css, 93 | ) 94 | 95 | 96 | register(CodeBlockAdapter(), CodeBlock) 97 | -------------------------------------------------------------------------------- /wagtailcodeblock/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | PRISM_PREFIX = "//cdnjs.cloudflare.com/ajax/libs/prism/" 4 | PRISM_VERSION = "1.29.0" 5 | 6 | 7 | def get_language_choices(): 8 | """ 9 | Default list of language choices, if not overridden by Django. 10 | """ 11 | DEFAULT_LANGUAGES = ( 12 | ("bash", "Bash/Shell"), 13 | ("css", "CSS"), 14 | ("diff", "diff"), 15 | ("html", "HTML"), 16 | ("javascript", "Javascript"), 17 | ("json", "JSON"), 18 | ("python", "Python"), 19 | ("scss", "SCSS"), 20 | ("yaml", "YAML"), 21 | ) 22 | 23 | return getattr(settings, "WAGTAIL_CODE_BLOCK_LANGUAGES", DEFAULT_LANGUAGES) 24 | 25 | 26 | def get_theme(): 27 | """ 28 | Returns a default theme, if not in the proejct's settings. Default theme is 'coy'. 29 | """ 30 | 31 | return getattr(settings, "WAGTAIL_CODE_BLOCK_THEME", "coy") 32 | 33 | 34 | def get_line_numbers(): 35 | """ 36 | Returns the line numbers setting. 37 | """ 38 | 39 | return getattr(settings, "WAGTAIL_CODE_BLOCK_LINE_NUMBERS", True) 40 | 41 | 42 | def get_copy_to_clipboard(): 43 | """ 44 | Returns the copy to clipboard setting. 45 | """ 46 | 47 | return getattr(settings, "WAGTAIL_CODE_BLOCK_COPY_TO_CLIPBOARD", True) 48 | -------------------------------------------------------------------------------- /wagtailcodeblock/static/wagtailcodeblock/css/wagtail-code-block.css: -------------------------------------------------------------------------------- 1 | .token.tag:before { 2 | content: none !important; 3 | } 4 | 5 | .token.tag { 6 | background-color: #fff !important; 7 | padding: 0 !important; 8 | } 9 | 10 | .code-block textarea { 11 | font-family: FreeMono, monospace; 12 | } 13 | 14 | .code-block code { 15 | display: block; 16 | } 17 | -------------------------------------------------------------------------------- /wagtailcodeblock/static/wagtailcodeblock/css/wagtail-code-block.min.css: -------------------------------------------------------------------------------- 1 | .token.tag:before{content:none!important}.token.tag{background-color:#fff!important;padding:0!important}.code-block textarea {font-family:FreeMono,monospace;}.code-block code{display:block;} -------------------------------------------------------------------------------- /wagtailcodeblock/static/wagtailcodeblock/js/wagtailcodeblock.js: -------------------------------------------------------------------------------- 1 | class CodeBlockDefinition extends window.wagtailStreamField.blocks 2 | .StructBlockDefinition { 3 | render(placeholder, prefix, initialState, initialError) { 4 | const block = super.render( 5 | placeholder, 6 | prefix, 7 | initialState, 8 | initialError, 9 | ); 10 | 11 | var languageField = $(document).find('#' + prefix + '-language'); 12 | var codeField = $(document).find('#' + prefix + '-code'); 13 | var targetField = $(document).find('#' + prefix + '-target'); 14 | 15 | function updateLanguage() { 16 | var languageCode = languageField.val(); 17 | targetField.removeClass().addClass('language-' + languageCode); 18 | prismRepaint(); 19 | } 20 | 21 | function prismRepaint() { 22 | Prism.highlightElement(targetField[0]); 23 | } 24 | 25 | function populateTargetCode() { 26 | var codeText = codeField.val(); 27 | targetField.text(codeText); 28 | prismRepaint(targetField); 29 | } 30 | 31 | updateLanguage(); 32 | populateTargetCode(); 33 | languageField.on('change', updateLanguage); 34 | codeField.on('keyup', populateTargetCode); 35 | 36 | return block; 37 | } 38 | } 39 | 40 | window.telepath.register('wagtailcodeblock.blocks.CodeBlock', CodeBlockDefinition); 41 | -------------------------------------------------------------------------------- /wagtailcodeblock/templates/wagtailcodeblock/code_block.html: -------------------------------------------------------------------------------- 1 | {% load static wagtailcodeblock_tags %} 2 | {% spaceless %} 3 | {% load_prism_css %} 4 | {% for key, val in self.items %} 5 | {% if key == "language" %} 6 | 48 | {% endif %} 49 | {% if key == "code" %} 50 |
51 |                 {{ val }}
52 |             
53 | 59 | {% endif %} 60 | {% endfor %} 61 | {% endspaceless %} 62 | -------------------------------------------------------------------------------- /wagtailcodeblock/templates/wagtailcodeblock/code_block_form.html: -------------------------------------------------------------------------------- 1 | {% load wagtailadmin_tags %} 2 | 3 |
4 | {% if help_text %} 5 |
6 | {% icon name="help" %} 7 | {{ help_text }} 8 |
9 | {% endif %} 10 | 11 | {% for child in children.values %} 12 |
13 | {% if child.block.label %} 14 | 15 | {% endif %} 16 | {{ child.render_form }} 17 | 18 | {% comment %} 19 | This block triggers the autosize() function by faking an input event after the 20 | document is fully loaded. It isn't pretty, but it works. 21 | {% endcomment %} 22 | {% if child.block.label == "Code" %} 23 | 30 | {% endif %} 31 |
32 | {% endfor %} 33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /wagtailcodeblock/templates/wagtailcodeblock/raw_code.html: -------------------------------------------------------------------------------- 1 | {% spaceless %} 2 | {% for key, val in self.items %} 3 | {% if key == "code" %} 4 | {{ val|safe }} 5 | {% endif %} 6 | {% endfor %} 7 | {% endspaceless %} -------------------------------------------------------------------------------- /wagtailcodeblock/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FlipperPA/wagtailcodeblock/11aaf676b236c22bceac8c9cf778a1f55003ccb9/wagtailcodeblock/templatetags/__init__.py -------------------------------------------------------------------------------- /wagtailcodeblock/templatetags/wagtailcodeblock_tags.py: -------------------------------------------------------------------------------- 1 | from django.template import Library 2 | from django.utils.safestring import mark_safe 3 | 4 | from ..settings import get_theme, get_line_numbers, get_copy_to_clipboard, PRISM_VERSION, PRISM_PREFIX 5 | 6 | register = Library() 7 | 8 | 9 | @register.simple_tag 10 | def prism_version(): 11 | """Returns the version of PrismJS.""" 12 | 13 | return PRISM_VERSION 14 | 15 | 16 | @register.simple_tag 17 | def line_numbers_js(): 18 | """Returns the JavaScript stanza to include the line numbers code.""" 19 | 20 | if get_line_numbers(): 21 | return mark_safe(f""", 22 | {{ 23 | "id": "code-block-line-numbers", 24 | "url": "//cdnjs.cloudflare.com/ajax/libs/prism/{PRISM_VERSION}/plugins/line-numbers/prism-line-numbers.min.js" 25 | }} 26 | """) 27 | else: 28 | return "" 29 | 30 | @register.simple_tag 31 | def copy_to_clipboard_js(): 32 | """Returns the JavaScript stanza to include the copy to clipboard code.""" 33 | 34 | if get_copy_to_clipboard(): 35 | return mark_safe(f""", 36 | {{ 37 | "id": "code-block-copy-to-clipboard", 38 | "url": "//cdnjs.cloudflare.com/ajax/libs/prism/{PRISM_VERSION}/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js" 39 | }} 40 | """) 41 | else: 42 | return "" 43 | 44 | @register.simple_tag 45 | def toolbar_js(): 46 | """Returns the JavaScript stanza to include the copy to clipboard code.""" 47 | 48 | if get_copy_to_clipboard(): 49 | return mark_safe(f""", 50 | {{ 51 | "id": "code-block-toolbar", 52 | "url": "//cdnjs.cloudflare.com/ajax/libs/prism/{PRISM_VERSION}/plugins/toolbar/prism-toolbar.min.js" 53 | }} 54 | """) 55 | else: 56 | return "" 57 | 58 | 59 | @register.simple_tag 60 | def load_prism_css(): 61 | """Loads the PrismJS theme.""" 62 | theme = get_theme() 63 | toolbar = True 64 | 65 | if theme: 66 | script = ( 67 | f"""""" 69 | ) 70 | else: 71 | script = ( 72 | f"""""" 74 | ) 75 | 76 | if get_line_numbers(): 77 | script += ( 78 | f"""""" 80 | ) 81 | 82 | if toolbar is True: 83 | script += ( 84 | f"""""" 86 | ) 87 | 88 | return mark_safe(script) 89 | -------------------------------------------------------------------------------- /wagtailcodeblock/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | from django.templatetags.static import static 2 | from django.utils.html import format_html_join 3 | 4 | from wagtail import hooks 5 | 6 | from .settings import get_theme, PRISM_VERSION, PRISM_PREFIX 7 | 8 | 9 | @hooks.register("insert_global_admin_css") 10 | def global_admin_css(): 11 | THEME = get_theme() 12 | 13 | if THEME: 14 | prism_theme = f"-{THEME}" 15 | else: 16 | prism_theme = "" 17 | 18 | extra_css = [ 19 | f"{PRISM_PREFIX}{PRISM_VERSION}/themes/prism{prism_theme}.min.css", 20 | static("wagtailcodeblock/css/wagtail-code-block.min.css"), 21 | ] 22 | 23 | return format_html_join( 24 | "\n", 25 | '', 26 | ((f,) for f in extra_css), 27 | ) 28 | 29 | 30 | @hooks.register("insert_global_admin_js") 31 | def global_admin_js(): 32 | """Add all prism languages""" 33 | 34 | js_files = [ 35 | f"{PRISM_PREFIX}{PRISM_VERSION}/prism.min.js", 36 | f"{PRISM_PREFIX}{PRISM_VERSION}/plugins/autoloader/prism-autoloader.min.js", 37 | ] 38 | 39 | js_includes = format_html_join( 40 | "\n", 41 | """""", 42 | ((f,) for f in js_files), 43 | ) 44 | 45 | return js_includes 46 | --------------------------------------------------------------------------------