diff --git a/base_requirements.txt b/base_requirements.txt index d11eff972..9bc2ff9ff 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -121,6 +121,10 @@ Pillow # https://github.com/psycopg/psycopg/blob/master/docs/news.rst psycopg[c,pool] +# Pygments syntax highlighting +# https://github.com/pygments/pygments/blob/master/CHANGES +Pygments + # YAML rendering library # https://github.com/yaml/pyyaml/blob/master/CHANGES PyYAML diff --git a/netbox/core/templatetags/__init__.py b/netbox/core/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/templatetags/highlight_code.py b/netbox/core/templatetags/highlight_code.py new file mode 100644 index 000000000..237a47a71 --- /dev/null +++ b/netbox/core/templatetags/highlight_code.py @@ -0,0 +1,30 @@ +from django import template +from django.utils.safestring import mark_safe +from pygments import highlight +from pygments.formatters import HtmlFormatter +from pygments.lexers import get_lexer_for_filename +from pygments.util import ClassNotFound + +register = template.Library() + + +@register.simple_tag +def highlight_code(value, filename: str): + """ + Highlight code using Pygments. + """ + if not value: + return mark_safe('
') + if not filename: + return mark_safe(f'{value}') # Fallback to plain text if no filename is provided + try: + lexer = get_lexer_for_filename(filename) + except ClassNotFound: + return mark_safe(f'
{value}') # Fallback to plain text if no lexer was found + return mark_safe( + highlight( + value, + lexer, + HtmlFormatter(linenos='inline', classprefix='pygments-', style='solarized-light'), + ) + ) diff --git a/netbox/core/tests/test_templatetags.py b/netbox/core/tests/test_templatetags.py new file mode 100644 index 000000000..0a1f9ce54 --- /dev/null +++ b/netbox/core/tests/test_templatetags.py @@ -0,0 +1,49 @@ +# ruff: noqa: E501 +from django.test import TestCase +from core.templatetags.highlight_code import highlight_code + + +FAKE_PLAIN_TEXT_NAME = 'fake_file.barbaz' +FAKE_PLAIN_TEXT_CONTENT = """\ +This is a fake text content for testing purposes. +""" + +FAKE_PYTHON_NAME = 'fake_file.py' +FAKE_PYTHON_CONTENT = """\ +def fake_function(): + print("This is a fake Python function.") +""" +FAKE_PYTHON_RESULT = """\ +
1def fake_function():\n2 print("This is a fake Python function.")\n
') and result.endswith('')) + self.assertTrue(FAKE_PLAIN_TEXT_CONTENT in result) + + def test_empty_content(self): + result = highlight_code('', 'FAKE_PLAIN_TEXT_NAME') + self.assertTrue(result.startswith('')) + self.assertTrue(len(result) == 11) + + result = highlight_code(None, 'FAKE_PLAIN_TEXT_NAME') + self.assertTrue(result.startswith('')) + self.assertTrue(len(result) == 11) + + def test_empty_filename(self): + result = highlight_code(' ', '') + self.assertTrue(result.startswith('
')) + self.assertTrue(len(result) == 12) + + result = highlight_code(' ', None) + self.assertTrue(result.startswith('
')) + self.assertTrue(len(result) == 12) diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index ab11dafff..e0937e9fe 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/styles/netbox.scss b/netbox/project-static/styles/netbox.scss index 6dbd34846..d2350df93 100644 --- a/netbox/project-static/styles/netbox.scss +++ b/netbox/project-static/styles/netbox.scss @@ -28,3 +28,7 @@ @import 'custom/misc'; @import 'custom/notifications'; @import 'custom/racks'; + +// Pygments styles +@import 'pygments/dark'; +@import 'pygments/light'; diff --git a/netbox/project-static/styles/pygments/README.md b/netbox/project-static/styles/pygments/README.md new file mode 100644 index 000000000..cc213892e --- /dev/null +++ b/netbox/project-static/styles/pygments/README.md @@ -0,0 +1,33 @@ +## Pygments style for NetBox + +The style are based on the `Pygments` themes `solarized-light` and `solarized-dark`. +To generated the scss files in this directory execute the following code in a Python environment where `Pygments` is installed: + +```python +from pygments.formatters import HtmlFormatter +h = HtmlFormatter(linenos="inline", classprefix="pygments-", style="solarized-dark") +print(h.get_style_defs()) + +h = HtmlFormatter(linenos="inline", classprefix="pygments-", style="solarized-light") +print(h.get_style_defs()) +``` + +To get the correct theme for dark and light modes wrap the resulting CSS in the following selectors: + +```scss +// _dark.scss +body[data-bs-theme='dark'] { + // Insert generated CSS for dark theme here +} + +// _light.scss +body[data-bs-theme='light'] { + // Insert generated CSS for light theme here +} +``` + +The run the formatter: + +```bash +yarn run format:styles +``` \ No newline at end of file diff --git a/netbox/project-static/styles/pygments/_dark.scss b/netbox/project-static/styles/pygments/_dark.scss new file mode 100644 index 000000000..610d86f0d --- /dev/null +++ b/netbox/project-static/styles/pygments/_dark.scss @@ -0,0 +1,277 @@ +body[data-bs-theme='dark'] { + td.linenos .normal { + color: #586e75; + background-color: #073642; + padding-left: 5px; + padding-right: 5px; + } + span.linenos { + color: #586e75; + background-color: #073642; + padding-left: 5px; + padding-right: 5px; + margin-right: 10px; + } + td.linenos .special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + span.linenos.special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + .hll { + background-color: #073642; + } + .pygments-c { + color: #586e75; + font-style: italic; + } /* Comment */ + .pygments-err { + color: #839496; + background-color: #dc322f; + } /* Error */ + .pygments-esc { + color: #839496; + } /* Escape */ + .pygments-g { + color: #839496; + } /* Generic */ + .pygments-k { + color: #859900; + } /* Keyword */ + .pygments-l { + color: #839496; + } /* Literal */ + .pygments-n { + color: #839496; + } /* Name */ + .pygments-o { + color: #586e75; + } /* Operator */ + .pygments-x { + color: #839496; + } /* Other */ + .pygments-p { + color: #839496; + } /* Punctuation */ + .pygments-ch { + color: #586e75; + font-style: italic; + } /* Comment.Hashbang */ + .pygments-cm { + color: #586e75; + font-style: italic; + } /* Comment.Multiline */ + .pygments-cp { + color: #d33682; + } /* Comment.Preproc */ + .pygments-cpf { + color: #586e75; + } /* Comment.PreprocFile */ + .pygments-c1 { + color: #586e75; + font-style: italic; + } /* Comment.Single */ + .pygments-cs { + color: #586e75; + font-style: italic; + } /* Comment.Special */ + .pygments-gd { + color: #dc322f; + } /* Generic.Deleted */ + .pygments-ge { + color: #839496; + font-style: italic; + } /* Generic.Emph */ + .pygments-ges { + color: #839496; + font-weight: bold; + font-style: italic; + } /* Generic.EmphStrong */ + .pygments-gr { + color: #dc322f; + } /* Generic.Error */ + .pygments-gh { + color: #839496; + font-weight: bold; + } /* Generic.Heading */ + .pygments-gi { + color: #859900; + } /* Generic.Inserted */ + .pygments-go { + color: #839496; + } /* Generic.Output */ + .pygments-gp { + color: #268bd2; + font-weight: bold; + } /* Generic.Prompt */ + .pygments-gs { + color: #839496; + font-weight: bold; + } /* Generic.Strong */ + .pygments-gu { + color: #839496; + text-decoration: underline; + } /* Generic.Subheading */ + .pygments-gt { + color: #268bd2; + } /* Generic.Traceback */ + .pygments-kc { + color: #2aa198; + } /* Keyword.Constant */ + .pygments-kd { + color: #2aa198; + } /* Keyword.Declaration */ + .pygments-kn { + color: #cb4b16; + } /* Keyword.Namespace */ + .pygments-kp { + color: #859900; + } /* Keyword.Pseudo */ + .pygments-kr { + color: #859900; + } /* Keyword.Reserved */ + .pygments-kt { + color: #b58900; + } /* Keyword.Type */ + .pygments-ld { + color: #839496; + } /* Literal.Date */ + .pygments-m { + color: #2aa198; + } /* Literal.Number */ + .pygments-s { + color: #2aa198; + } /* Literal.String */ + .pygments-na { + color: #839496; + } /* Name.Attribute */ + .pygments-nb { + color: #268bd2; + } /* Name.Builtin */ + .pygments-nc { + color: #268bd2; + } /* Name.Class */ + .pygments-no { + color: #268bd2; + } /* Name.Constant */ + .pygments-nd { + color: #268bd2; + } /* Name.Decorator */ + .pygments-ni { + color: #268bd2; + } /* Name.Entity */ + .pygments-ne { + color: #268bd2; + } /* Name.Exception */ + .pygments-nf { + color: #268bd2; + } /* Name.Function */ + .pygments-nl { + color: #268bd2; + } /* Name.Label */ + .pygments-nn { + color: #268bd2; + } /* Name.Namespace */ + .pygments-nx { + color: #839496; + } /* Name.Other */ + .pygments-py { + color: #839496; + } /* Name.Property */ + .pygments-nt { + color: #268bd2; + } /* Name.Tag */ + .pygments-nv { + color: #268bd2; + } /* Name.Variable */ + .pygments-ow { + color: #859900; + } /* Operator.Word */ + .pygments-pm { + color: #839496; + } /* Punctuation.Marker */ + .pygments-w { + color: #839496; + } /* Text.Whitespace */ + .pygments-mb { + color: #2aa198; + } /* Literal.Number.Bin */ + .pygments-mf { + color: #2aa198; + } /* Literal.Number.Float */ + .pygments-mh { + color: #2aa198; + } /* Literal.Number.Hex */ + .pygments-mi { + color: #2aa198; + } /* Literal.Number.Integer */ + .pygments-mo { + color: #2aa198; + } /* Literal.Number.Oct */ + .pygments-sa { + color: #2aa198; + } /* Literal.String.Affix */ + .pygments-sb { + color: #2aa198; + } /* Literal.String.Backtick */ + .pygments-sc { + color: #2aa198; + } /* Literal.String.Char */ + .pygments-dl { + color: #2aa198; + } /* Literal.String.Delimiter */ + .pygments-sd { + color: #586e75; + } /* Literal.String.Doc */ + .pygments-s2 { + color: #2aa198; + } /* Literal.String.Double */ + .pygments-se { + color: #2aa198; + } /* Literal.String.Escape */ + .pygments-sh { + color: #2aa198; + } /* Literal.String.Heredoc */ + .pygments-si { + color: #2aa198; + } /* Literal.String.Interpol */ + .pygments-sx { + color: #2aa198; + } /* Literal.String.Other */ + .pygments-sr { + color: #cb4b16; + } /* Literal.String.Regex */ + .pygments-s1 { + color: #2aa198; + } /* Literal.String.Single */ + .pygments-ss { + color: #2aa198; + } /* Literal.String.Symbol */ + .pygments-bp { + color: #268bd2; + } /* Name.Builtin.Pseudo */ + .pygments-fm { + color: #268bd2; + } /* Name.Function.Magic */ + .pygments-vc { + color: #268bd2; + } /* Name.Variable.Class */ + .pygments-vg { + color: #268bd2; + } /* Name.Variable.Global */ + .pygments-vi { + color: #268bd2; + } /* Name.Variable.Instance */ + .pygments-vm { + color: #268bd2; + } /* Name.Variable.Magic */ + .pygments-il { + color: #2aa198; + } /* Literal.Number.Integer.Long */ +} diff --git a/netbox/project-static/styles/pygments/_light.scss b/netbox/project-static/styles/pygments/_light.scss new file mode 100644 index 000000000..d7d540983 --- /dev/null +++ b/netbox/project-static/styles/pygments/_light.scss @@ -0,0 +1,277 @@ +body[data-bs-theme='light'] { + td.linenos .normal { + color: #93a1a1; + background-color: #eee8d5; + padding-left: 5px; + padding-right: 5px; + } + span.linenos { + color: #93a1a1; + background-color: #eee8d5; + padding-left: 5px; + padding-right: 5px; + margin-right: 10px; + } + td.linenos .special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + span.linenos.special { + color: #000000; + background-color: #ffffc0; + padding-left: 5px; + padding-right: 5px; + } + .hll { + background-color: #eee8d5; + } + .pygments-c { + color: #93a1a1; + font-style: italic; + } /* Comment */ + .pygments-err { + color: #657b83; + background-color: #dc322f; + } /* Error */ + .pygments-esc { + color: #657b83; + } /* Escape */ + .pygments-g { + color: #657b83; + } /* Generic */ + .pygments-k { + color: #859900; + } /* Keyword */ + .pygments-l { + color: #657b83; + } /* Literal */ + .pygments-n { + color: #657b83; + } /* Name */ + .pygments-o { + color: #93a1a1; + } /* Operator */ + .pygments-x { + color: #657b83; + } /* Other */ + .pygments-p { + color: #657b83; + } /* Punctuation */ + .pygments-ch { + color: #93a1a1; + font-style: italic; + } /* Comment.Hashbang */ + .pygments-cm { + color: #93a1a1; + font-style: italic; + } /* Comment.Multiline */ + .pygments-cp { + color: #d33682; + } /* Comment.Preproc */ + .pygments-cpf { + color: #93a1a1; + } /* Comment.PreprocFile */ + .pygments-c1 { + color: #93a1a1; + font-style: italic; + } /* Comment.Single */ + .pygments-cs { + color: #93a1a1; + font-style: italic; + } /* Comment.Special */ + .pygments-gd { + color: #dc322f; + } /* Generic.Deleted */ + .pygments-ge { + color: #657b83; + font-style: italic; + } /* Generic.Emph */ + .pygments-ges { + color: #657b83; + font-weight: bold; + font-style: italic; + } /* Generic.EmphStrong */ + .pygments-gr { + color: #dc322f; + } /* Generic.Error */ + .pygments-gh { + color: #657b83; + font-weight: bold; + } /* Generic.Heading */ + .pygments-gi { + color: #859900; + } /* Generic.Inserted */ + .pygments-go { + color: #657b83; + } /* Generic.Output */ + .pygments-gp { + color: #268bd2; + font-weight: bold; + } /* Generic.Prompt */ + .pygments-gs { + color: #657b83; + font-weight: bold; + } /* Generic.Strong */ + .pygments-gu { + color: #657b83; + text-decoration: underline; + } /* Generic.Subheading */ + .pygments-gt { + color: #268bd2; + } /* Generic.Traceback */ + .pygments-kc { + color: #2aa198; + } /* Keyword.Constant */ + .pygments-kd { + color: #2aa198; + } /* Keyword.Declaration */ + .pygments-kn { + color: #cb4b16; + } /* Keyword.Namespace */ + .pygments-kp { + color: #859900; + } /* Keyword.Pseudo */ + .pygments-kr { + color: #859900; + } /* Keyword.Reserved */ + .pygments-kt { + color: #b58900; + } /* Keyword.Type */ + .pygments-ld { + color: #657b83; + } /* Literal.Date */ + .pygments-m { + color: #2aa198; + } /* Literal.Number */ + .pygments-s { + color: #2aa198; + } /* Literal.String */ + .pygments-na { + color: #657b83; + } /* Name.Attribute */ + .pygments-nb { + color: #268bd2; + } /* Name.Builtin */ + .pygments-nc { + color: #268bd2; + } /* Name.Class */ + .pygments-no { + color: #268bd2; + } /* Name.Constant */ + .pygments-nd { + color: #268bd2; + } /* Name.Decorator */ + .pygments-ni { + color: #268bd2; + } /* Name.Entity */ + .pygments-ne { + color: #268bd2; + } /* Name.Exception */ + .pygments-nf { + color: #268bd2; + } /* Name.Function */ + .pygments-nl { + color: #268bd2; + } /* Name.Label */ + .pygments-nn { + color: #268bd2; + } /* Name.Namespace */ + .pygments-nx { + color: #657b83; + } /* Name.Other */ + .pygments-py { + color: #657b83; + } /* Name.Property */ + .pygments-nt { + color: #268bd2; + } /* Name.Tag */ + .pygments-nv { + color: #268bd2; + } /* Name.Variable */ + .pygments-ow { + color: #859900; + } /* Operator.Word */ + .pygments-pm { + color: #657b83; + } /* Punctuation.Marker */ + .pygments-w { + color: #657b83; + } /* Text.Whitespace */ + .pygments-mb { + color: #2aa198; + } /* Literal.Number.Bin */ + .pygments-mf { + color: #2aa198; + } /* Literal.Number.Float */ + .pygments-mh { + color: #2aa198; + } /* Literal.Number.Hex */ + .pygments-mi { + color: #2aa198; + } /* Literal.Number.Integer */ + .pygments-mo { + color: #2aa198; + } /* Literal.Number.Oct */ + .pygments-sa { + color: #2aa198; + } /* Literal.String.Affix */ + .pygments-sb { + color: #2aa198; + } /* Literal.String.Backtick */ + .pygments-sc { + color: #2aa198; + } /* Literal.String.Char */ + .pygments-dl { + color: #2aa198; + } /* Literal.String.Delimiter */ + .pygments-sd { + color: #93a1a1; + } /* Literal.String.Doc */ + .pygments-s2 { + color: #2aa198; + } /* Literal.String.Double */ + .pygments-se { + color: #2aa198; + } /* Literal.String.Escape */ + .pygments-sh { + color: #2aa198; + } /* Literal.String.Heredoc */ + .pygments-si { + color: #2aa198; + } /* Literal.String.Interpol */ + .pygments-sx { + color: #2aa198; + } /* Literal.String.Other */ + .pygments-sr { + color: #cb4b16; + } /* Literal.String.Regex */ + .pygments-s1 { + color: #2aa198; + } /* Literal.String.Single */ + .pygments-ss { + color: #2aa198; + } /* Literal.String.Symbol */ + .pygments-bp { + color: #268bd2; + } /* Name.Builtin.Pseudo */ + .pygments-fm { + color: #268bd2; + } /* Name.Function.Magic */ + .pygments-vc { + color: #268bd2; + } /* Name.Variable.Class */ + .pygments-vg { + color: #268bd2; + } /* Name.Variable.Global */ + .pygments-vi { + color: #268bd2; + } /* Name.Variable.Instance */ + .pygments-vm { + color: #268bd2; + } /* Name.Variable.Magic */ + .pygments-il { + color: #2aa198; + } /* Literal.Number.Integer.Long */ +} diff --git a/netbox/templates/core/datafile.html b/netbox/templates/core/datafile.html index 175a0e2bc..efe6566e9 100644 --- a/netbox/templates/core/datafile.html +++ b/netbox/templates/core/datafile.html @@ -4,6 +4,7 @@ {% load helpers %} {% load perms %} {% load plugins %} +{% load highlight_code %} {% load i18n %} {% block breadcrumbs %} @@ -54,7 +55,7 @@
{{ object.data_as_string }}+ {% highlight_code object.data_as_string object.path %}