From 80f42c8b2a8a9db6eaa409a7639b0bf5c73e8f72 Mon Sep 17 00:00:00 2001 From: Tobias Genannt Date: Thu, 14 Aug 2025 19:53:06 +0200 Subject: [PATCH] Closes #20013: Syntax highlighting on Data Files Add syntax highlighting on the details page for Data Files. --- base_requirements.txt | 4 + netbox/core/templatetags/__init__.py | 0 netbox/core/templatetags/highlight_code.py | 30 ++ netbox/project-static/dist/netbox.css | Bin 555502 -> 563987 bytes netbox/project-static/styles/netbox.scss | 4 + .../project-static/styles/pygments/README.md | 33 +++ .../project-static/styles/pygments/_dark.scss | 277 ++++++++++++++++++ .../styles/pygments/_light.scss | 277 ++++++++++++++++++ netbox/templates/core/datafile.html | 3 +- requirements.txt | 1 + 10 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 netbox/core/templatetags/__init__.py create mode 100644 netbox/core/templatetags/highlight_code.py create mode 100644 netbox/project-static/styles/pygments/README.md create mode 100644 netbox/project-static/styles/pygments/_dark.scss create mode 100644 netbox/project-static/styles/pygments/_light.scss diff --git a/base_requirements.txt b/base_requirements.txt index 8d74ede6c..062dc4ea3 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -125,6 +125,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..065065c92 --- /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 '' + 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/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 4777dffa26b35622975a5b2f3ff36c92fc9a654b..cc4d82694c3103c60f815b59f121a1a4a4ce636c 100644 GIT binary patch delta 5202 zcmcImOOG2x5LO=SS_uK6-AGw)5Qe=#vbOB;+H3Ddz+d2!T)52J6MK3_ZpZNoA?4sZ zLXqM>f;e!39|S@|;tcNUsjBI2yg`C%cRjxPs;c|@?T?RN|MNcpL@Wx~>WE^V`?GJtVphyAP6nf4d_K4g{BWAiix(nl>x=Gr ze|Y9ymVOjvB5lv&WN|SlS9mbb(uq3R_|F^7i7b6l<@oPnU*`Ti%|siL^}6LsWjo;Y z$TAKyf42GZS)=(8htbhD6`XFjyZL>xfB0#VBw-f|*W|OHLkICu*!R3dIlfDdGTat; z`~XkVdLU&_w^O_wjK_c1y@h=TeHB?OmOyI-%7|4|w@{=JYh~anD{ClFS>MIX;Y67~ zrh~k)-$~&SIbedQ-ye=Vm7vM4lX^fCK+gmq>-LxK9uj~wsvKCSsGu~iu62jq@tz>! zG)4s6<7KQC?7Z8(9uW=|L-ZLXn79y8e8mu}FGF*h9d?c5z#7bwVSBvcD2O}&ZzN;p z*VJxwH+_NFV0~V{`m?!x;+hQLqOt)5#sExVk;s0E{q%glHy#;uOmR8F;4J73&2Xif zCl(HyF>=v2MRdVnRBa}C&kzh_mliiSmJQi+LKl9;1PeyHC;}CY9RM}#pfD{W^B_|P zgivh0c-DBxLL{aKJU&#iK!Tn967ax~eR;LJs)b5O_F`Gtv-osfysWNSzX*S6QcmXd zR+4#ca+;g<=kNkD7odAAOhw#$^|Wzcb-&5{95M`o-(yBID>S}{J3`|(3``JRJoL&6jo-5o`aK3HoPgUY z6&vC%Tu=Nl4~6+itpNiMYgQ=Q%xY@5dNwY67ycbCmxu#gZH9}qoSqt|mIJ@fYkmle zJCX|8HMg9;6rV{_c98BdPlp1}osDLr7TcloAU2=i6z9wsm>K}=GQ5;7(dU4D4M_x> z&k9LEU2G)*57hS6DnzYC4>-W!Q#IAA-K1%ENkVYs;a$qm5x#_><>9fbBsm=65j`nI zu;Tq8!D#e{KLa^HIHC%XditI1*rXqg*qlBQK=6AMTxdr5QDIv}_EBaZ#r9ERAL%!g zk?fL{c6Wq+AsIEKFLzbF01P~#LucAub;(T|R;fG2z8ln?J5c3zFLIl#TtDxuJ-JPM zZg))j7V*-~Sbdd@8kZWisUcCi>l2Wk4Z@xgY1b%qjSQDdq&jfnx{_A4TPF4A#KZsN omhlA&^aH`#k?8ibqouu5%hY0GY1imdv4H+V_Jx>Y_S-vu0Z-WV*Z=?k delta 27 icmbQdP3hfd#fBEf7N!>F7M2#)7Pc1lEgWwv8My$QED6&9 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 0747547b1..656fc724f 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 %} @@ -48,7 +49,7 @@

{% trans "Content" %}

-
{{ object.data_as_string }}
+ {% highlight_code object.data_as_string object.path %}
{% plugin_left_page object %} diff --git a/requirements.txt b/requirements.txt index 700d147b6..df02b3632 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,6 +29,7 @@ netaddr==1.3.0 nh3==0.3.0 Pillow==11.3.0 psycopg[c,pool]==3.2.9 +pygments==2.19.2 PyYAML==6.0.2 requests==2.32.4 rq==2.4.1