From cc82ab19ab539ea3cab46e45009f9c9815412d4b Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 3 Sep 2025 03:21:34 +0200 Subject: [PATCH] feat: syntax highlighting in field description (#33791) --- .../doctype/server_script/server_script.js | 57 ++++++++++--------- .../doctype/client_script/client_script.js | 35 ++++++------ .../doctype/notification/notification.js | 10 +++- .../js/frappe/form/controls/base_input.js | 12 +++- frappe/public/js/frappe/utils/utils.js | 25 ++++++++ .../public/js/syntax_highlighting.bundle.js | 12 ++++ frappe/public/scss/desk.bundle.scss | 1 + 7 files changed, 102 insertions(+), 50 deletions(-) create mode 100644 frappe/public/js/syntax_highlighting.bundle.js diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 86b99c1c12..736e9b5346 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -44,16 +44,15 @@ frappe.ui.form.on("Server Script", { }, setup_help(frm) { - frm.get_field("help_html").html(` + const help_field = frm.get_field("help_html"); + help_field.html(`

DocType Event

Add logic for standard doctype events like Before Insert, After Submit, etc.

-
-	
+

 # set property
 if "test" in doc.description:
 	doc.status = 'Closed'
 
-
 # validate
 if "validate" in doc.description:
 	raise frappe.ValidationError
@@ -65,13 +64,11 @@ if doc.allocated_to:
 		owner = doc.allocated_to,
 		description = doc.subject
 	)).insert()
-
-
+
Payment processing

Payment processing events have a special state. See the PaymentController in Frappe Payments for details.

-
-	
+

 # retreive payment session state
 ps = doc.flags.payment_session
 
@@ -81,7 +78,10 @@ if ps.is_success:
 	# custom process return values
 	doc.flags.payment_session.result = {
 		"message": "Thank you for your payment",
-		"action": {"href": "https://shop.example.com", "label": "Return to shop"},
+		"action": {
+			"href": "https://shop.example.com",
+			"label": "Return to shop"
+		}
 	}
 if ps.is_pre_authorized:
 	if ps.changed: # could be an idempotent run
@@ -92,21 +92,20 @@ if ps.is_processing:
 if ps.is_declined:
 	if ps.changed: # could be an idempotent run
 		...
-
-
+
+

The On Payment Failed (on_payment_failed) event only transports the error message which the controller implementation had extracted from the transaction.

-
-	
+
+

 msg = doc.flags.payment_failure_message
 doc.my_failure_message_field = msg
-
-
+

API Call

Respond to /api/method/<method-name> calls, just like whitelisted methods

-

+

 # respond to API
 
 if frappe.form_dict.message == "ping":
@@ -119,12 +118,16 @@ else:
 
 

Permission Query

Add conditions to the where clause of list queries.

-

-# generate dynamic conditions and set it in the conditions variable
-tenant_id = frappe.db.get_value(...)
-conditions = f'tenant_id = {tenant_id}'
+

Generate dynamic conditions and set it in the conditions variable:

-# resulting select query +

+tenant_id = frappe.db.get_value(...) # -> 2
+conditions = f'tenant_id = {tenant_id}'
+
+ +

The resulting select query is:

+ +

 select name from \`tabPerson\`
 where tenant_id = 2
 order by creation desc
@@ -135,15 +138,13 @@ order by creation desc
 

Workflow Task

Execute when a particular Workflow Action Master is executed.

Gets the document which the action is being applied on in the doc variable.

-
+

 # create a customer with the same name as the given document
-
 customer = frappe.new_doc("Customer")
-customer.customer_name = doc.first_name + " " + doc.last_name # we get this from the workflow action
+customer.customer_name = doc.first_name + " " + doc.last_name # we get this doc from the workflow action
 customer.customer_type = "Company"
-
-c.save()
-
-`); +customer.save() +
`); + frappe.utils.highlight_pre(help_field.$wrapper); }, }); diff --git a/frappe/custom/doctype/client_script/client_script.js b/frappe/custom/doctype/client_script/client_script.js index 481aa3ffd7..a09e1e72b2 100644 --- a/frappe/custom/doctype/client_script/client_script.js +++ b/frappe/custom/doctype/client_script/client_script.js @@ -3,7 +3,9 @@ frappe.ui.form.on("Client Script", { setup(frm) { - frm.get_field("sample").html(SAMPLE_HTML); + const sample_field = frm.get_field("sample"); + sample_field.html(SAMPLE_HTML); + frappe.utils.highlight_pre(sample_field.$wrapper); }, refresh(frm) { if (frm.doc.dt && frm.doc.script) { @@ -106,53 +108,50 @@ frappe.ui.form.on('${doctype}', { const SAMPLE_HTML = `

Client Script Help

Client Scripts are executed only on the client-side (i.e. in Forms). Here are some examples to get you started

-

-
+

 // fetch local_tax_no on selection of customer
-// cur_frm.add_fetch(link_field,  source_fieldname,  target_fieldname);
-cur_frm.add_fetch("customer",  "local_tax_no',  'local_tax_no');
+// cur_frm.add_fetch(link_field, source_fieldname, target_fieldname);
+cur_frm.add_fetch("customer", "local_tax_no", "local_tax_no");
 
 // additional validation on dates
-frappe.ui.form.on('Task',  'validate',  function(frm) {
+frappe.ui.form.on("Task", "validate", function(frm) {
     if (frm.doc.from_date < get_today()) {
-        msgprint('You can not select past date in From Date');
+        msgprint("You can not select past date in From Date");
         validated = false;
     }
 });
 
 // make a field read-only after saving
-frappe.ui.form.on('Task',  {
+frappe.ui.form.on("Task",  {
     refresh: function(frm) {
-        // use the __islocal value of doc,  to check if the doc is saved or not
-        frm.set_df_property('myfield',  'read_only',  frm.doc.__islocal ? 0 : 1);
+        frm.set_df_property("myfield",  "read_only",  frm.is_new() ? 0 : 1);
     }
 });
 
 // additional permission check
-frappe.ui.form.on('Task',  {
+frappe.ui.form.on("Task",  {
     validate: function(frm) {
-        if(user=='user1@example.com' && frm.doc.purpose!='Material Receipt') {
-            msgprint('You are only allowed Material Receipt');
+        if(user === "user1@example.com" && frm.doc.purpose !== "Material Receipt") {
+            msgprint("You are only allowed Material Receipt");
             validated = false;
         }
     }
 });
 
 // calculate sales incentive
-frappe.ui.form.on('Sales Invoice',  {
+frappe.ui.form.on("Sales Invoice",  {
     validate: function(frm) {
         // calculate incentives for each person on the deal
         total_incentive = 0
-        $.each(frm.doc.sales_team,  function(i,  d) {
+        for (const row of frm.doc.sales_team) {
             // calculate incentive
             var incentive_percent = 2;
             if(frm.doc.base_grand_total > 400) incentive_percent = 4;
             // actual incentive
-            d.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
-            total_incentive += flt(d.incentives)
+            row.incentives = flt(frm.doc.base_grand_total) * incentive_percent / 100;
+            total_incentive += flt(row.incentives)
         });
         frm.doc.total_incentive = total_incentive;
     }
 })
-
 
`; diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 9f430e9143..518b06daaa 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -112,7 +112,7 @@ frappe.notification = { if (frm.doc.channel === "Email") { template = `
Message Example
-
<h3>Order Overdue</h3>
+
<h3>Order Overdue</h3>
 
 <p>Transaction {{ doc.name }} has exceeded Due Date. Please take necessary action.</p>
 
@@ -127,7 +127,7 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
 <li>Customer: {{ doc.customer }}</li>
 <li>Amount: {{ doc.grand_total }}</li>
 </ul>
-
+
`; } else if (["Slack", "System Notification", "SMS"].includes(frm.doc.channel)) { template = `
Message Example
@@ -148,7 +148,11 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
`; } if (template) { - frm.set_df_property("message_examples", "options", template); + const message_examples_field = frm.get_field("message_examples"); + message_examples_field.html(template); + if (frm.doc.channel === "Email") { + frappe.utils.highlight_pre(message_examples_field.$wrapper); + } } }, }; diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 152a04ac0a..f6d63b4120 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -206,7 +206,17 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control return; } if (this.df.description) { - this.$wrapper.find(".help-box").html(__(this.df.description, null, this.df.parent)); + const description = __(this.df.description, null, this.df.parent); + const help_box = this.$wrapper.find(".help-box"); + help_box.html(description); + if (description.includes(" { + help_box.find("code").each(function () { + hljs.highlightElement(this); + this.style.display = "inline"; // override hljs's "block" display + }); + }); + } this.toggle_description(true); } else { this.set_empty_description(); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 37ff0925f0..4c0787bdff 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1837,4 +1837,29 @@ Object.assign(frappe.utils, { } } }, + + /** + * Adds syntax highlighting to all
 tags in the given jQuery wrapper.
+	 * Example wrapper:
+	 *
+	 * ```html
+	 * 

+	 * def add(a, b):
+	 *     return a + b
+	 *
+	 * print(add(1, 2))
+	 *
+	 * # Output: 3
+	 * 
+ * ``` + * + * @param {jQuery} $wrapper - The jQuery wrapper to add syntax highlighting to. + */ + highlight_pre($wrapper) { + frappe.require("syntax_highlighting.bundle.js").then(() => { + $wrapper.find("pre").each(function () { + hljs.highlightElement(this); + }); + }); + }, }); diff --git a/frappe/public/js/syntax_highlighting.bundle.js b/frappe/public/js/syntax_highlighting.bundle.js new file mode 100644 index 0000000000..d0b6326b9d --- /dev/null +++ b/frappe/public/js/syntax_highlighting.bundle.js @@ -0,0 +1,12 @@ +import hljs from "highlight.js/lib/core"; +import javascript from "highlight.js/lib/languages/javascript"; +import python from "highlight.js/lib/languages/python"; +import xml from "highlight.js/lib/languages/xml"; +import sql from "highlight.js/lib/languages/sql"; + +hljs.registerLanguage("javascript", javascript); +hljs.registerLanguage("python", python); +hljs.registerLanguage("xml", xml); +hljs.registerLanguage("sql", sql); + +window.hljs = hljs; diff --git a/frappe/public/scss/desk.bundle.scss b/frappe/public/scss/desk.bundle.scss index cbf365861a..6b673619c0 100644 --- a/frappe/public/scss/desk.bundle.scss +++ b/frappe/public/scss/desk.bundle.scss @@ -9,3 +9,4 @@ @import "frappe/public/js/lib/leaflet_easy_button/easy-button.css"; @import "frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.css"; @import "frappe/public/js/lib/leaflet_draw/leaflet.draw.css"; +@import "frappe/public/node_modules/highlight.js/styles/tomorrow.css";