From db1353cf635008d66ec34589ca19cf93a164c8d9 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 17 Sep 2025 17:10:11 -0500 Subject: [PATCH] Enable source code syntax highlighting Add highlight.js to the build Modify CSS to clean up renderer source code Minor HTML clean ups Specify language for source blocks in some recent posts --- .../2024-01-23-grpc-jakarta-rs-arrays.md | 24 +- ...-15-resteasy-spring-boot-with-springdoc.md | 2 +- .../2025-02-14-resteasy-grpc-collections.md | 38 +- pom.xml | 20 +- public/css/org_common.css | 8 + src/main/resources/application.properties | 4 + .../web/app/highlightjs-line-numbers.js | 375 ++++++++++++++++++ src/main/resources/web/app/main.js | 8 + templates/partials/head.html | 9 +- 9 files changed, 443 insertions(+), 45 deletions(-) create mode 100644 src/main/resources/web/app/highlightjs-line-numbers.js create mode 100644 src/main/resources/web/app/main.js diff --git a/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md b/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md index e297bb0..7baf1bf 100644 --- a/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md +++ b/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md @@ -9,7 +9,7 @@ author: Ron Sigal Release 1.0.0.Alpha5 of resteasy-grpc has a new feature for handling arbitrary arrays. Although protobuf comes with a representation of one dimension arrays, e.g. -``` +```protobuf message ints { repeated int64 is = 1; } @@ -25,7 +25,7 @@ The mechanism has two parts: arrays.proto looks like this: -``` +```protobuf message dev_resteasy_grpc_arrays___BooleanArray { repeated bool bool_field = 1; } @@ -66,7 +66,7 @@ It starts with a definition of array message types for Then, `dev_resteasy_grpc_arrays___ArrayHolder` is defined with a oneof field that can hold any of these array message types. The self-referential field -``` +```protobuf dev_resteasy_grpc_arrays___ArrayHolderArray arrayHolderArray_field = 13; ``` @@ -77,7 +77,7 @@ Compiling arrays.proto generates `dev.resteasy.grpc.arrays.Array_proto`, which g gateway into the javabuf[^javabuf] world. Suppose we want to generate a representation of `int[] {3, 5}`. That would look like -``` +```java dev_resteasy_grpc_arrays___IntArray.Builder iab = dev_resteasy_grpc_arrays___IntArray.newBuilder(); iab.addIntField(3); iab.addIntField(5); @@ -105,13 +105,13 @@ of `int[][] {{3, 5}, {7, 11, 13}}`. To avoid the mess, grpc-bridge-runtime includes the class `dev.resteasy.grpc.arrays.ArrayUtility`. With `ArrayUtility`, building the javabuf representation of `int[][] {{3, 5}, {7, 11, 13}}` is as easy as -``` +```java dev_resteasy_grpc_arrays___ArrayHolder holder = ArrayUtility.getHolder(new int[][] {{3, 5}, {7, 11, 13}}); ``` Moreover, `ArrayUtility` can turn the `dev_resteasy_grpc_arrays___ArrayHolder` back to the original array: -``` +```java Object array = ArrayUtility.getArray(holder); Assert.assertArrayEquals(new int[][] {{3, 5}, {7, 11, 13}}, (int[][]) array); ``` @@ -119,20 +119,20 @@ Moreover, `ArrayUtility` can turn the `dev_resteasy_grpc_arrays___ArrayHolder` b These two calls to `ArrayUtility` depend on the fact that the target array is built from a primitive Java type. If the array uses an application specific type, then there are two alternative calls that can be used: -``` +```java public static dev_resteasy_grpc_arrays___ArrayHolder getHolder(JavabufTranslator translator, Object o); ``` and -``` +```java public static Object getArray(JavabufTranslator translator, Array_proto.dev_resteasy_grpc_arrays___ArrayHolder ah) throws Exception; ``` Also, if an application uses arrays, the generated `JavabufTranslator` incorporates `ArrayUtility`, so that it can be used instead: -``` +```java dev_resteasy_grpc_arrays___ArrayHolder ah = (dev_resteasy_grpc_arrays___ArrayHolder) translator.translateToJavabuf(new int[][] {{3, 5}, {7, 11, 13}}); Object array = translator.translateFromJavabuf(ah); Assert.assertArrayEquals(new int[][] {{3, 5}, {7, 11, 13}}, (int[][]) array); @@ -141,7 +141,7 @@ used instead: **Note.** The latter point can be usefully expanded, independent of the presence of arrays. Consider the class -``` +```java package dev.resteasy.grpc.example; public class C { @@ -160,7 +160,7 @@ used instead: Using the fluent methods created in, say, `C_proto` by the protobuf parser, an instance of `C_proto.dev_resteasy_grpc_example___C` can be created by -``` +```java C_proto.dev_resteasy_grpc_example___C.Builder cb = C_proto.dev_resteasy_grpc_example___C.newBuilder(); C_proto.dev_resteasy_grpc_example___C c1 = cb.setI(3).setD(5.0).setS("seven").build(); ``` @@ -168,7 +168,7 @@ Using the fluent methods created in, say, `C_proto` by the protobuf parser, an i Note that each field must be set individually. On the other hand, given the `C(int, double, String)` constructor, an instance of `C_proto.dev_resteasy_grpc_example___C` can be created more directly: -``` +```java C_proto.dev_resteasy_grpc_example___C c2 = (C_proto.dev_resteasy_grpc_example___C) translator.translateToJavabuf(new C(3, 5.0, "seven")); ``` diff --git a/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md b/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md index 707b8af..054b597 100644 --- a/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md +++ b/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md @@ -20,7 +20,7 @@ The [WildFly](https://github.com/wildfly/wildfly) and [Quarkus](https://github.c - [Using MicroProfile OpenAPI With RESTEasy](https://resteasy.dev/2023/02/20/resteasy-microprofile-openapi/) -For Quarkus, here is a blog post that shows the usage of the built-in MicroProfile OpenAPI in Quarkus: +For Quarkus, here is a blog post that shows the usage of the built-in MicroProfile OpenAPI in Quarkus: - [MicroProfile OpenAPI for everyone](https://quarkus.io/blog/openapi-for-everyone/) diff --git a/content/posts/2025-02-14-resteasy-grpc-collections.md b/content/posts/2025-02-14-resteasy-grpc-collections.md index 7b25d0b..e401b7d 100644 --- a/content/posts/2025-02-14-resteasy-grpc-collections.md +++ b/content/posts/2025-02-14-resteasy-grpc-collections.md @@ -13,7 +13,7 @@ and `java.util.Set`. In order to handle arbitrary implementations, idiosyncratic implementating classes are ignored and all implementations are assigned the least common nature of lists and sets. That is, an implementation of `java.util.List` is considered to be an ordered sequence and is translated to a protobuf message type of the form -``` +```protobuf message java_util___ArrayList16 { string classname = 1; repeated int32 data = 2; @@ -21,7 +21,7 @@ message type of the form ``` and an implementation of `java.util.Set` is considered to be an unordered collection and is translated to a protobuf message type of the form -``` +```protobuf message java_util___HashSet3 { string classname = 1; repeated string data = 2; @@ -40,7 +40,7 @@ a workaround. The important thing is to define a protobuf message whose repeated `java_util___ArrayList16`. That would be a reasonable representation of `ArrayList`. Similarly, `java_util___HashSet3` would be a reasonable representation of `HashSet`. The `JavaToProtobufGenerator` class in the [grpc-bridge](https://github.com/resteasy/resteasy-grpc) module generates the protobuf messages and decorates them as follows: -``` +```java // List: java.util.ArrayList message java_util___ArrayList16 { string classname = 1; @@ -56,7 +56,7 @@ would be a reasonable representation of `HashSet`. The `JavaToProtobufGe } ``` These are two simple examples. Consider something a little more complicated: `java.util.ArrayList>`: -``` +```java // List: java.util.ArrayList> message java_util___ArrayList14 { string classname = 1; @@ -70,7 +70,7 @@ field in `java_util___ArrayList14` is of type `java_util___HashSet3`. A complication arises in the form of type variables and wildcards. The solution adopted in resteasy-grpc is to map unassigned type variables and wildcards to `java.lang.Object`, which makes sense, since they can take any types at runtime. The protobuf analog to `java.lang.Object` is `google.protobuf.Any`, which is defined -``` +```protobuf message Any { string type_url = 1; bytes value = 2; @@ -81,7 +81,7 @@ package declared in the .proto file. The value field has built-in type bytes, w of bytes no longer than 2^32", according to https://developers.google.com/protocol-buffers/docs/proto3. Suppose we have the Jakarta REST resource methods -``` +```java package x.y; @GET @@ -110,13 +110,13 @@ Suppose we have the Jakarta REST resource methods } ``` where x.y.Grimble is -``` +```java public class Grimble { T t; } ``` `JavaToProtobufGenerator` would create the rpc and message definitions -``` +```protobuf // p/grimble/raw x_y___Grimble google.protobuf.Empty GET sync rpc gr_raw (GeneralEntityMessage) returns (GeneralReturnMessage); @@ -155,7 +155,7 @@ where x.y.Grimble is ``` The details about the rpc comments are described elsewhere ([gRPC Bridge Project: User Guide](https://resteasy.dev/docs/grpc/)), but here it's enough to know that the rpc definition -``` +```protobuf // p/grimble/variable x_y___Grimble18 google.protobuf.Empty GET sync rpc gr_variable (GeneralEntityMessage) returns (GeneralReturnMessage); ``` @@ -175,7 +175,7 @@ type `google.protobuf.Any`, which, as discussed above, represents an arbitrary t `of java.lang.Object`. The discussion about generic types and type variables applies to lists and sets. For example, -``` +```java @Path("arraylist/hashset/wildcard") @POST public ArrayList> arraylistHashsetTest2(ArrayList> l) { @@ -183,7 +183,7 @@ The discussion about generic types and type variables applies to lists and sets. } ``` gives rise to -``` +```java // List: java.util.ArrayList> message java_util___ArrayList13 { string classname = 1; @@ -202,7 +202,7 @@ gives rise to Here we'll discuss a gRPC client intending to communicate with a Jakarta REST server. The subject is covered in detail in [gRPC Bridge Project: User Guide](https://resteasy.dev/docs/grpc/), but here we will look at sending and receiving `Collection`s. For example, consider the resource method -``` +```java @GET @Path("arraylist/integer") public ArrayList listArray0(ArrayList list) { @@ -212,7 +212,7 @@ Here we'll discuss a gRPC client intending to communicate with a Jakarta REST se We've seen that `java.util.ArrayList` translates to javabuf class `java_util___ArrayList16`. So the client has to create an instance of `java_util___ArrayList16` to send to the server. There are two possible strategies. One is to work in the javabuf world: -``` +```java java_util___ArrayList16.Builder juaBuilder = java_util___ArrayList16.newBuilder(); juaBuilder.setClassname("java.util.ArrayList"); juaBuilder.addData(3); @@ -220,7 +220,7 @@ work in the javabuf world: java_util___ArrayList16 jua = juaBuilder.build(); ``` Alternatively, one could create an `ArrayList` and translate it to an `java_util___ArrayList16`: -``` +```java ArrayList list = new ArrayList(); list.add(3); list.add(7); @@ -229,18 +229,18 @@ Alternatively, one could create an `ArrayList` and translate it to an `java_util ``` where `translator` is an instance of `dev.resteasy.grpc.bridge.runtime.protobuf.JavabufTranslator`. The next step is to build a `GeneralEntityMessage`: -``` +```java GeneralEntityMessage.Builder gemBuilder = GeneralEntityMessage.newBuilder(); gemBuilder.setJavaUtilArrayList16Field(gemBuilder.build()); ``` Then the remote method can be invoked: -``` +```java GeneralReturnMessage response = stub.listArray0(gem); ``` where `stub` is the client side representative of the server methods. Finally, the result can be extracted from the `GeneralReturnMessage`. Note that `listArray0` returns an instance of `ArrayList`, which translates to javabuf class -``` +```java // List: java.util.ArrayList message java_util___ArrayList17 { string classname = 1; @@ -249,11 +249,11 @@ an instance of `ArrayList`, which translates to javabuf class } ``` This complicates things a bit since we have to extract the returned list from an `Any`. -``` +```java java_util___ArrayList17 result = response.getJavaUtilArrayList17Field(); Any any = response.getAnyField(); Message result = any.unpack((Class) Utility.extractClassFromAny(any, translator)); list = (ArrayList) translator.translateFromJavabuf(result); ``` ### Maps -Stay tuned for the next release, which which implementations of `java.util.Map` will be treated in a similar way. \ No newline at end of file +Stay tuned for the next release, in which implementations of `java.util.Map` will be treated similarly. diff --git a/pom.xml b/pom.xml index 556c8c7..3bf4af6 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,10 @@ + + io.quarkus + quarkus-arc + io.quarkiverse.roq quarkus-roq @@ -58,11 +62,12 @@ quarkus-roq-plugin-tagging ${version.quarkus-roq} - - io.quarkus - quarkus-arc + io.quarkiverse.roq + quarkus-roq-plugin-sitemap + ${version.quarkus-roq} + com.fasterxml.jackson.dataformat jackson-dataformat-yaml @@ -71,12 +76,13 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - - io.quarkiverse.roq - quarkus-roq-plugin-sitemap - ${version.quarkus-roq} + org.mvnpm + highlight.js + 11.11.1 + provided + io.quarkus quarkus-junit5 diff --git a/public/css/org_common.css b/public/css/org_common.css index 7a7c70c..99ce490 100644 --- a/public/css/org_common.css +++ b/public/css/org_common.css @@ -94,6 +94,14 @@ These are the common css styles for the JBoss.org site. /* = Common tags and elements */ +pre:has(code) { + margin: 0px; + max-height: max-content; + width: auto; + padding: 0px; + border: none; +} + pre { margin: 0px; padding: 10px; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a094ff9..1144eca 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,7 @@ site.escaped-pages=posts/*.md,docs/**.adoc quarkus.log.category."io.quarkiverse.roq.frontmatter.deployment".level=DEBUG + +quarkus.web-bundler.dependencies.auto-import=all +quarkus.asciidoc.attributes.icons=font +quarkus.asciidoc.attributes.source-highlighter=highlight.js diff --git a/src/main/resources/web/app/highlightjs-line-numbers.js b/src/main/resources/web/app/highlightjs-line-numbers.js new file mode 100644 index 0000000..b5dd51c --- /dev/null +++ b/src/main/resources/web/app/highlightjs-line-numbers.js @@ -0,0 +1,375 @@ +// jshint multistr:true + +function installLineNumbers (hljs, w, d) { + 'use strict'; + + var TABLE_NAME = 'hljs-ln', + LINE_NAME = 'hljs-ln-line', + CODE_BLOCK_NAME = 'hljs-ln-code', + NUMBERS_BLOCK_NAME = 'hljs-ln-numbers', + NUMBER_LINE_NAME = 'hljs-ln-n', + DATA_ATTR_NAME = 'data-line-number', + BREAK_LINE_REGEXP = /\r\n|\r|\n/g; + + if (hljs) { + hljs.initLineNumbersOnLoad = initLineNumbersOnLoad; + hljs.lineNumbersBlock = lineNumbersBlock; + hljs.lineNumbersBlockSync = lineNumbersBlockSync; + hljs.lineNumbersValue = lineNumbersValue; + + addStyles(); + } else { + w.console.error('highlight.js not detected!'); + } + + function isHljsLnCodeDescendant(domElt) { + var curElt = domElt; + while (curElt) { + if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) { + return true; + } + curElt = curElt.parentNode; + } + return false; + } + + function getHljsLnTable(hljsLnDomElt) { + var curElt = hljsLnDomElt; + while (curElt.nodeName !== 'TABLE') { + curElt = curElt.parentNode; + } + return curElt; + } + + // Function to workaround a copy issue with Microsoft Edge. + // Due to hljs-ln wrapping the lines of code inside a element, + // itself wrapped inside a
 element, window.getSelection().toString()
+    // does not contain any line breaks. So we need to get them back using the
+    // rendered code in the DOM as reference.
+    function edgeGetSelectedCodeLines(selection) {
+        // current selected text without line breaks
+        var selectionText = selection.toString();
+
+        // get the 
' + + '' + + '' + + '', + [ + LINE_NAME, + NUMBERS_BLOCK_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME, + CODE_BLOCK_NAME, + i + options.startFrom, + lines[i].length > 0 ? lines[i] : ' ' + ]); + } + + return format('
element wrapping the first line of selected code + var tdAnchor = selection.anchorNode; + while (tdAnchor.nodeName !== 'TD') { + tdAnchor = tdAnchor.parentNode; + } + + // get the element wrapping the last line of selected code + var tdFocus = selection.focusNode; + while (tdFocus.nodeName !== 'TD') { + tdFocus = tdFocus.parentNode; + } + + // extract line numbers + var firstLineNumber = parseInt(tdAnchor.dataset.lineNumber); + var lastLineNumber = parseInt(tdFocus.dataset.lineNumber); + + // multi-lines copied case + if (firstLineNumber != lastLineNumber) { + + var firstLineText = tdAnchor.textContent; + var lastLineText = tdFocus.textContent; + + // if the selection was made backward, swap values + if (firstLineNumber > lastLineNumber) { + var tmp = firstLineNumber; + firstLineNumber = lastLineNumber; + lastLineNumber = tmp; + tmp = firstLineText; + firstLineText = lastLineText; + lastLineText = tmp; + } + + // discard not copied characters in first line + while (selectionText.indexOf(firstLineText) !== 0) { + firstLineText = firstLineText.slice(1); + } + + // discard not copied characters in last line + while (selectionText.lastIndexOf(lastLineText) === -1) { + lastLineText = lastLineText.slice(0, -1); + } + + // reconstruct and return the real copied text + var selectedText = firstLineText; + var hljsLnTable = getHljsLnTable(tdAnchor); + for (var i = firstLineNumber + 1 ; i < lastLineNumber ; ++i) { + var codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]); + var codeLineElt = hljsLnTable.querySelector(codeLineSel); + selectedText += '\n' + codeLineElt.textContent; + } + selectedText += '\n' + lastLineText; + return selectedText; + // single copied line case + } else { + return selectionText; + } + } + + // ensure consistent code copy/paste behavior across all browsers + // (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51) + document.addEventListener('copy', function(e) { + // get current selection + var selection = window.getSelection(); + // override behavior when one wants to copy line of codes + if (isHljsLnCodeDescendant(selection.anchorNode)) { + var selectionText; + // workaround an issue with Microsoft Edge as copied line breaks + // are removed otherwise from the selection string + if (window.navigator.userAgent.indexOf('Edge') !== -1) { + selectionText = edgeGetSelectedCodeLines(selection); + } else { + // other browsers can directly use the selection string + selectionText = selection.toString(); + } + e.clipboardData.setData('text/plain', selectionText); + e.preventDefault(); + } + }); + + function addStyles () { + var css = d.createElement('style'); + css.type = 'text/css'; + css.innerHTML = format( + '.{0}{border-collapse:collapse}' + + '.{0} td{padding:0}' + + '.{1}:before{content:attr({2})}', + [ + TABLE_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME + ]); + d.getElementsByTagName('head')[0].appendChild(css); + } + + function initLineNumbersOnLoad (options) { + if (d.readyState === 'interactive' || d.readyState === 'complete') { + documentReady(options); + } else { + w.addEventListener('DOMContentLoaded', function () { + documentReady(options); + }); + } + } + + function documentReady (options) { + try { + var blocks = d.querySelectorAll('code.hljs,code.nohighlight'); + + for (var i in blocks) { + if (blocks.hasOwnProperty(i)) { + if (!isPluginDisabledForBlock(blocks[i])) { + lineNumbersBlock(blocks[i], options); + } + } + } + } catch (e) { + w.console.error('LineNumbers error: ', e); + } + } + + function isPluginDisabledForBlock(element) { + return element.classList.contains('nohljsln'); + } + + function lineNumbersBlock (element, options) { + if (typeof element !== 'object') return; + + async(function () { + element.innerHTML = lineNumbersInternal(element, options); + }); + } + + function lineNumbersBlockSync (element, options) { + if (typeof element !== 'object') return; + + element.innerHTML = lineNumbersInternal(element, options); + } + + function lineNumbersValue (value, options) { + if (typeof value !== 'string') return; + + var element = document.createElement('code') + element.innerHTML = value + + return lineNumbersInternal(element, options); + } + + function lineNumbersInternal (element, options) { + + var internalOptions = mapOptions(element, options); + + duplicateMultilineNodes(element); + + return addLineNumbersBlockFor(element.innerHTML, internalOptions); + } + + function addLineNumbersBlockFor (inputHtml, options) { + var lines = getLines(inputHtml); + + // if last line contains only carriage return remove it + if (lines[lines.length-1].trim() === '') { + lines.pop(); + } + + if (lines.length > 1 || options.singleLine) { + var html = ''; + + for (var i = 0, l = lines.length; i < l; i++) { + html += format( + '
' + + '
' + + '
' + + '{6}' + + '
{1}
', [ TABLE_NAME, html ]); + } + + return inputHtml; + } + + /** + * @param {HTMLElement} element Code block. + * @param {Object} options External API options. + * @returns {Object} Internal API options. + */ + function mapOptions (element, options) { + options = options || {}; + return { + singleLine: getSingleLineOption(options), + startFrom: getStartFromOption(element, options) + }; + } + + function getSingleLineOption (options) { + var defaultValue = true; + if (!!options.singleLine) { + return options.singleLine; + } + return defaultValue; + } + + function getStartFromOption (element, options) { + var defaultValue = 1; + var startFrom = defaultValue; + + if (isFinite(options.startFrom)) { + startFrom = options.startFrom; + } + + // can be overridden because local option is priority + var value = getAttribute(element, 'data-ln-start-from'); + if (value !== null) { + startFrom = toNumber(value, defaultValue); + } + + return startFrom; + } + + /** + * Recursive method for fix multi-line elements implementation in highlight.js + * Doing deep passage on child nodes. + * @param {HTMLElement} element + */ + function duplicateMultilineNodes (element) { + var nodes = element.childNodes; + for (var node in nodes) { + if (nodes.hasOwnProperty(node)) { + var child = nodes[node]; + if (getLinesCount(child.textContent) > 0) { + if (child.childNodes.length > 0) { + duplicateMultilineNodes(child); + } else { + duplicateMultilineNode(child.parentNode); + } + } + } + } + } + + /** + * Method for fix multi-line elements implementation in highlight.js + * @param {HTMLElement} element + */ + function duplicateMultilineNode (element) { + var className = element.className; + + if ( ! /hljs-/.test(className)) return; + + var lines = getLines(element.innerHTML); + + for (var i = 0, result = ''; i < lines.length; i++) { + var lineText = lines[i].length > 0 ? lines[i] : ' '; + result += format('{1}\n', [ className, lineText ]); + } + + element.innerHTML = result.trim(); + } + + function getLines (text) { + if (text.length === 0) return []; + return text.split(BREAK_LINE_REGEXP); + } + + function getLinesCount (text) { + return (text.trim().match(BREAK_LINE_REGEXP) || []).length; + } + + /// + /// HELPERS + /// + + function async (func) { + w.setTimeout(func, 0); + } + + /** + * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript} + * @param {string} format + * @param {array} args + */ + function format (format, args) { + return format.replace(/\{(\d+)\}/g, function(m, n){ + return args[n] !== undefined ? args[n] : m; + }); + } + + /** + * @param {HTMLElement} element Code block. + * @param {String} attrName Attribute name. + * @returns {String} Attribute value or empty. + */ + function getAttribute (element, attrName) { + return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null; + } + + /** + * @param {String} str Source string. + * @param {Number} fallback Fallback value. + * @returns Parsed number or fallback value. + */ + function toNumber (str, fallback) { + if (!str) return fallback; + var number = Number(str); + return isFinite(number) ? number : fallback; + } + +} + +export default installLineNumbers; diff --git a/src/main/resources/web/app/main.js b/src/main/resources/web/app/main.js new file mode 100644 index 0000000..fb49957 --- /dev/null +++ b/src/main/resources/web/app/main.js @@ -0,0 +1,8 @@ +import hljs from 'highlight.js'; +import installLineNumbers from './highlightjs-line-numbers.js'; +import 'highlight.js/styles/agate.css'; + +installLineNumbers(hljs, window, document); + +hljs.highlightAll(); +hljs.initLineNumbersOnLoad(); diff --git a/templates/partials/head.html b/templates/partials/head.html index e8b105a..16931ed 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -1,9 +1,7 @@ - - - - + + + {#bundle /} {#seo page site /} {#rss site /} @@ -20,5 +18,4 @@ -