From 13481100f0116dab64b4f78217d695ccb35099d2 Mon Sep 17 00:00:00 2001 From: Jason Summers <3616919+jasonsummers@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:43:41 +0000 Subject: [PATCH 1/2] refactor: remove JSPool Package BREAKING: GetJavaScriptEnginePool signature in IExecutionState.cs changed as follows: - renamed to GetJavaScriptEngine - All parameters removed and replaced with a single Action parameter which is used to configure the engine. - removes the JSPool package from Statiq.Core.csproj. - bumps JavaScriptEngineSwitcher packages to latest versions. - removes all classes related to JavaScript Engine Pooling. - updates consuming code in the Statiq.Highlight extension. --- .../Execution/EmptyExecutionContext.cs | 4 +- .../Execution/IExecutionState.cs | 26 +-- .../JavaScript/IJavaScriptEngine.cs | 5 +- .../JavaScript/IJavaScriptEnginePool.cs | 36 ----- src/core/Statiq.Core/Execution/Engine.cs | 35 ++-- .../Statiq.Core/Execution/ExecutionContext.cs | 9 +- .../JavaScript/JavaScriptEnginePool.cs | 81 ---------- .../JavaScript/PooledJavaScriptEngine.cs | 151 ------------------ src/core/Statiq.Core/Statiq.Core.csproj | 3 +- .../Statiq.Testing.JavaScript.csproj | 4 +- .../Statiq.Testing/Execution/TestEngine.cs | 13 +- .../Execution/TestExecutionContext.cs | 9 +- .../Execution/TestJsEnginePool.cs | 53 ------ .../Statiq.Highlight/HighlightCode.cs | 49 +++--- .../Statiq.Highlight/HighlightShortcode.cs | 4 +- 15 files changed, 69 insertions(+), 413 deletions(-) delete mode 100644 src/core/Statiq.Common/JavaScript/IJavaScriptEnginePool.cs delete mode 100644 src/core/Statiq.Core/JavaScript/JavaScriptEnginePool.cs delete mode 100644 src/core/Statiq.Core/JavaScript/PooledJavaScriptEngine.cs delete mode 100644 src/core/Statiq.Testing/Execution/TestJsEnginePool.cs diff --git a/src/core/Statiq.Common/Execution/EmptyExecutionContext.cs b/src/core/Statiq.Common/Execution/EmptyExecutionContext.cs index 0a256ae44..f3a22ccef 100644 --- a/src/core/Statiq.Common/Execution/EmptyExecutionContext.cs +++ b/src/core/Statiq.Common/Execution/EmptyExecutionContext.cs @@ -90,8 +90,8 @@ public Task> ExecuteModulesAsync(IEnumerable public Stream GetContentStream(string content = null) => ExecutionState.GetContentStream(content); - public IJavaScriptEnginePool GetJavaScriptEnginePool(Action initializer = null, int startEngines = 10, int maxEngines = 25, int maxUsagesPerEngine = 100, TimeSpan? engineTimeout = null) => - ExecutionState.GetJavaScriptEnginePool(initializer, startEngines, maxEngines, maxUsagesPerEngine, engineTimeout); + public IJavaScriptEngine GetJavaScriptEngine(Action configureEngine = null) => + ExecutionState.GetJavaScriptEngine(configureEngine); public Task SendHttpRequestWithRetryAsync(Func requestFactory) => ExecutionState.SendHttpRequestWithRetryAsync(requestFactory); diff --git a/src/core/Statiq.Common/Execution/IExecutionState.cs b/src/core/Statiq.Common/Execution/IExecutionState.cs index 99f1ea99d..34bacebd7 100644 --- a/src/core/Statiq.Common/Execution/IExecutionState.cs +++ b/src/core/Statiq.Common/Execution/IExecutionState.cs @@ -182,26 +182,10 @@ internal set Task SendHttpRequestWithRetryAsync(Func requestFactory, int retryCount); /// - /// Gets a new . The returned engine pool should be disposed - /// when no longer needed. - /// - /// - /// The code to run when a new engine is created. This should configure - /// the environment and set up any required JavaScript libraries. - /// - /// The number of engines to initially start when a pool is created. - /// The maximum number of engines that will be created in the pool. - /// The maximum number of times an engine can be reused before it is disposed. - /// - /// The default timeout to use when acquiring an engine from the pool (defaults to 5 seconds). - /// If an engine can not be acquired in this time frame, an exception will be thrown. - /// - /// A new JavaScript engine pool. - IJavaScriptEnginePool GetJavaScriptEnginePool( - Action initializer = null, - int startEngines = 10, - int maxEngines = 25, - int maxUsagesPerEngine = 100, - TimeSpan? engineTimeout = null); + /// Gets a new . + /// + /// Optional delegate to configure the engine. + /// A new JavaScript engine. + IJavaScriptEngine GetJavaScriptEngine(Action configureEngine = null); } } \ No newline at end of file diff --git a/src/core/Statiq.Common/JavaScript/IJavaScriptEngine.cs b/src/core/Statiq.Common/JavaScript/IJavaScriptEngine.cs index 69b48aa04..af3f91102 100644 --- a/src/core/Statiq.Common/JavaScript/IJavaScriptEngine.cs +++ b/src/core/Statiq.Common/JavaScript/IJavaScriptEngine.cs @@ -5,10 +5,7 @@ namespace Statiq.Common { /// - /// A common interface to a JavaScript engine. Every JavaScript engine is - /// obtained from a and will be returned to the - /// pool when it is disposed. Therefore, you must dispose the engine when - /// you are done with it. + /// A common interface to a JavaScript engine. /// public interface IJavaScriptEngine : IDisposable { diff --git a/src/core/Statiq.Common/JavaScript/IJavaScriptEnginePool.cs b/src/core/Statiq.Common/JavaScript/IJavaScriptEnginePool.cs deleted file mode 100644 index a1244dd80..000000000 --- a/src/core/Statiq.Common/JavaScript/IJavaScriptEnginePool.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace Statiq.Common -{ - /// - /// Provides a shared pool of JavaScript engine instances. You should dispose the pool when - /// no longer needed to properly dispose of any allocated engines. - /// - public interface IJavaScriptEnginePool : IDisposable - { - /// - /// Gets an engine from the pool. This engine should be disposed when you are finished with it. - /// If an engine is free, this method returns immediately with the engine. - /// If no engines are available but we have not reached the maximum number of engines - /// yet, creates a new engine. If the maximum number of engines has been reached, blocks until an engine is - /// available again. - /// - /// - /// Maximum time to wait for a free engine. If not specified, defaults to the timeout - /// specified when creating the pool. - /// - /// A JavaScript engine. - IJavaScriptEngine GetEngine(TimeSpan? timeout = null); - - /// - /// Disposes the specified engine and removes it from the pool. A new engine will be created in it's place. - /// - /// The JavaScript engine. - void RecycleEngine(IJavaScriptEngine engine); - - /// - /// Disposes all engines in this pool, and creates new engines in their place. - /// - void RecycleAllEngines(); - } -} diff --git a/src/core/Statiq.Core/Execution/Engine.cs b/src/core/Statiq.Core/Execution/Engine.cs index e8cf12d21..fb08a9e06 100644 --- a/src/core/Statiq.Core/Execution/Engine.cs +++ b/src/core/Statiq.Core/Execution/Engine.cs @@ -10,6 +10,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using JavaScriptEngineSwitcher.Core; +using JavaScriptEngineSwitcher.Jint; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -36,6 +38,8 @@ public class Engine : IEngine, IDisposable // Gets initialized on first execute and reset when the pipeline collection changes private PipelinePhase[] _phases; + private static readonly object JsEngineSwitcherLock = new object(); + private bool _disposed; /// @@ -1057,18 +1061,25 @@ public async Task SendHttpRequestWithRetryAsync(Func - public IJavaScriptEnginePool GetJavaScriptEnginePool( - Action initializer = null, - int startEngines = 10, - int maxEngines = 25, - int maxUsagesPerEngine = 100, - TimeSpan? engineTimeout = null) => - new JavaScriptEnginePool( - initializer, - startEngines, - maxEngines, - maxUsagesPerEngine, - engineTimeout ?? TimeSpan.FromSeconds(5)); + public IJavaScriptEngine GetJavaScriptEngine(Action configureEngine = null) + { + // First we need to check if the JsEngineSwitcher has been configured. We'll do this + // by checking the DefaultEngineName being set. If that's there we can safely assume + // its been configured somehow (maybe via a configuration file). If not we'll wire up + // Jint as the default engine. + lock (JsEngineSwitcherLock) + { + if (string.IsNullOrWhiteSpace(JsEngineSwitcher.Current.DefaultEngineName)) + { + JsEngineSwitcher.Current.EngineFactories.Add(new JintJsEngineFactory()); + JsEngineSwitcher.Current.DefaultEngineName = JintJsEngine.EngineName; + } + } + + IJavaScriptEngine engine = new JavaScriptEngine(JsEngineSwitcher.Current.CreateDefaultEngine()); + configureEngine?.Invoke(engine); + return engine; + } /// /// Applies settings for analyzers and log levels as "[analyzer]=[log level]" (log level is optional, "All" to set all analyzers). diff --git a/src/core/Statiq.Core/Execution/ExecutionContext.cs b/src/core/Statiq.Core/Execution/ExecutionContext.cs index 2da0ef04c..9efdce3af 100644 --- a/src/core/Statiq.Core/Execution/ExecutionContext.cs +++ b/src/core/Statiq.Core/Execution/ExecutionContext.cs @@ -162,13 +162,8 @@ public async Task> ExecuteModulesAsync(IEnumerable _contextData.Engine.GetContentStream(content); /// - public IJavaScriptEnginePool GetJavaScriptEnginePool( - Action initializer = null, - int startEngines = 10, - int maxEngines = 25, - int maxUsagesPerEngine = 100, - TimeSpan? engineTimeout = null) => - _contextData.Engine.GetJavaScriptEnginePool(initializer, startEngines, maxEngines, maxUsagesPerEngine, engineTimeout); + public IJavaScriptEngine GetJavaScriptEngine(Action configureEngine = null) => + _contextData.Engine.GetJavaScriptEngine(configureEngine); // IDocumentFactory diff --git a/src/core/Statiq.Core/JavaScript/JavaScriptEnginePool.cs b/src/core/Statiq.Core/JavaScript/JavaScriptEnginePool.cs deleted file mode 100644 index 44525bdf9..000000000 --- a/src/core/Statiq.Core/JavaScript/JavaScriptEnginePool.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using Statiq.Common; -using JavaScriptEngineSwitcher.Core; -using JavaScriptEngineSwitcher.Jint; -using JSPool; -using IJavaScriptEngine = Statiq.Common.IJavaScriptEngine; - -namespace Statiq.Core -{ - internal class JavaScriptEnginePool : IJavaScriptEnginePool - { - private static readonly object EngineSwitcherLock = new object(); - - private readonly JsPool _pool; - private bool _disposed = false; - - public JavaScriptEnginePool( - Action initializer, - int startEngines, - int maxEngines, - int maxUsagesPerEngine, - in TimeSpan engineTimeout) - { - // First we need to check if the JsEngineSwitcher has been configured. We'll do this - // by checking the DefaultEngineName being set. If that's there we can safely assume - // its been configured somehow (maybe via a configuration file). If not we'll wire up - // Jint as the default engine. - lock (EngineSwitcherLock) - { - if (string.IsNullOrWhiteSpace(JsEngineSwitcher.Current.DefaultEngineName)) - { - JsEngineSwitcher.Current.EngineFactories.Add(new JintJsEngineFactory()); - JsEngineSwitcher.Current.DefaultEngineName = JintJsEngine.EngineName; - } - } - - _pool = new JsPool(new JsPoolConfig - { - EngineFactory = () => new JavaScriptEngine(JsEngineSwitcher.Current.CreateDefaultEngine()), - Initializer = x => initializer?.Invoke(x), - StartEngines = startEngines, - MaxEngines = maxEngines, - MaxUsagesPerEngine = maxUsagesPerEngine, - GetEngineTimeout = engineTimeout - }); - } - - public void Dispose() - { - CheckDisposed(); - _pool.Dispose(); - _disposed = true; - } - - public IJavaScriptEngine GetEngine(TimeSpan? timeout = null) => new PooledJavaScriptEngine(_pool.GetEngine(timeout), _pool); - - public void RecycleEngine(IJavaScriptEngine engine) - { - engine.ThrowIfNull(nameof(engine)); - if (!(engine is PooledJavaScriptEngine pooledEngine)) - { - throw new ArgumentException("The specified engine was not from a pool"); - } - if (pooledEngine.Pool != _pool) - { - throw new ArgumentException("The specified engine is from a different pool"); - } - _pool.DisposeEngine(pooledEngine.Engine); - } - - public void RecycleAllEngines() => _pool.Recycle(); - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(JsPool)); - } - } - } -} \ No newline at end of file diff --git a/src/core/Statiq.Core/JavaScript/PooledJavaScriptEngine.cs b/src/core/Statiq.Core/JavaScript/PooledJavaScriptEngine.cs deleted file mode 100644 index c79a9a9b3..000000000 --- a/src/core/Statiq.Core/JavaScript/PooledJavaScriptEngine.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Reflection; -using System.Text; -using JSPool; -using Statiq.Common; - -namespace Statiq.Core -{ - /// - /// Wraps a but overrides the - /// dispose behavior so that instead of disposing the - /// underlying engine, it returns the engine to the pool. - /// - internal class PooledJavaScriptEngine : IJavaScriptEngine - { - private bool _disposed = false; - - public PooledJavaScriptEngine(JavaScriptEngine engine, JsPool pool) - { - Engine = engine; - Pool = pool; - } - - internal JavaScriptEngine Engine { get; } - - internal JsPool Pool { get; } - - public void Dispose() - { - CheckDisposed(); - Pool.ReturnEngineToPool(Engine); - _disposed = true; - } - - public string Name - { - get - { - CheckDisposed(); - return Engine.Name; - } - } - - public string Version - { - get - { - CheckDisposed(); - return Engine.Version; - } - } - - public object Evaluate(string expression) - { - CheckDisposed(); - return Engine.Evaluate(expression); - } - - public T Evaluate(string expression) - { - CheckDisposed(); - return Engine.Evaluate(expression); - } - - public void Execute(string code) - { - CheckDisposed(); - Engine.Execute(code); - } - - public void ExecuteFile(string path, Encoding encoding = null) - { - CheckDisposed(); - Engine.ExecuteFile(path, encoding); - } - - public void ExecuteResource(string resourceName, Type type) - { - CheckDisposed(); - Engine.ExecuteResource(resourceName, type); - } - - public void ExecuteResource(string resourceName, Assembly assembly) - { - CheckDisposed(); - Engine.ExecuteResource(resourceName, assembly); - } - - public object CallFunction(string functionName, params object[] args) - { - CheckDisposed(); - return Engine.CallFunction(functionName, args); - } - - public T CallFunction(string functionName, params object[] args) - { - CheckDisposed(); - return Engine.CallFunction(functionName, args); - } - - public bool HasVariable(string variableName) - { - CheckDisposed(); - return Engine.HasVariable(variableName); - } - - public object GetVariableValue(string variableName) - { - CheckDisposed(); - return Engine.GetVariableValue(variableName); - } - - public T GetVariableValue(string variableName) - { - CheckDisposed(); - return Engine.GetVariableValue(variableName); - } - - public void SetVariableValue(string variableName, object value) - { - CheckDisposed(); - Engine.SetVariableValue(variableName, value); - } - - public void RemoveVariable(string variableName) - { - CheckDisposed(); - Engine.RemoveVariable(variableName); - } - - public void EmbedHostObject(string itemName, object value) - { - CheckDisposed(); - Engine.EmbedHostObject(itemName, value); - } - - public void EmbedHostType(string itemName, Type type) - { - CheckDisposed(); - Engine.EmbedHostType(itemName, type); - } - - private void CheckDisposed() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(PooledJavaScriptEngine)); - } - } - } -} \ No newline at end of file diff --git a/src/core/Statiq.Core/Statiq.Core.csproj b/src/core/Statiq.Core/Statiq.Core.csproj index f5834e7d5..0f7917b91 100644 --- a/src/core/Statiq.Core/Statiq.Core.csproj +++ b/src/core/Statiq.Core/Statiq.Core.csproj @@ -5,8 +5,7 @@ - - + diff --git a/src/core/Statiq.Testing.JavaScript/Statiq.Testing.JavaScript.csproj b/src/core/Statiq.Testing.JavaScript/Statiq.Testing.JavaScript.csproj index f7a724490..b9bbb6c5a 100644 --- a/src/core/Statiq.Testing.JavaScript/Statiq.Testing.JavaScript.csproj +++ b/src/core/Statiq.Testing.JavaScript/Statiq.Testing.JavaScript.csproj @@ -4,8 +4,8 @@ Statiq Static StaticContent StaticSite Blog BlogEngine - - + + diff --git a/src/core/Statiq.Testing/Execution/TestEngine.cs b/src/core/Statiq.Testing/Execution/TestEngine.cs index 71fd12bb2..b8f5e670a 100644 --- a/src/core/Statiq.Testing/Execution/TestEngine.cs +++ b/src/core/Statiq.Testing/Execution/TestEngine.cs @@ -195,13 +195,12 @@ public async Task SendHttpRequestWithRetryAsync(Func - public IJavaScriptEnginePool GetJavaScriptEnginePool( - Action initializer = null, - int startEngines = 10, - int maxEngines = 25, - int maxUsagesPerEngine = 100, - TimeSpan? engineTimeout = null) => - new TestJsEnginePool(JsEngineFunc, initializer); + public IJavaScriptEngine GetJavaScriptEngine(Action configureEngine = null) + { + IJavaScriptEngine engine = JsEngineFunc(); + configureEngine?.Invoke(engine); + return engine; + } public Func JsEngineFunc { get; set; } = () => throw new NotImplementedException("JavaScript test engine not initialized. Statiq.Testing.JavaScript can be used to return a working JavaScript engine"); diff --git a/src/core/Statiq.Testing/Execution/TestExecutionContext.cs b/src/core/Statiq.Testing/Execution/TestExecutionContext.cs index 6ae43f445..5272f3568 100644 --- a/src/core/Statiq.Testing/Execution/TestExecutionContext.cs +++ b/src/core/Statiq.Testing/Execution/TestExecutionContext.cs @@ -305,13 +305,8 @@ public async Task> ExecuteModulesAsync(IEnumerable - public IJavaScriptEnginePool GetJavaScriptEnginePool( - Action initializer = null, - int startEngines = 10, - int maxEngines = 25, - int maxUsagesPerEngine = 100, - TimeSpan? engineTimeout = null) => - Engine.GetJavaScriptEnginePool(initializer, startEngines, maxEngines, maxUsagesPerEngine, engineTimeout); + public IJavaScriptEngine GetJavaScriptEngine(Action configureEngine = null) => + Engine.GetJavaScriptEngine(configureEngine); public Func JsEngineFunc { diff --git a/src/core/Statiq.Testing/Execution/TestJsEnginePool.cs b/src/core/Statiq.Testing/Execution/TestJsEnginePool.cs deleted file mode 100644 index 47829c7e1..000000000 --- a/src/core/Statiq.Testing/Execution/TestJsEnginePool.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Statiq.Common; - -namespace Statiq.Testing -{ - public class TestJsEnginePool : IJavaScriptEnginePool - { - private readonly Func _engineFunc; - private readonly Action _initializer; - - public TestJsEnginePool(Func engineFunc, Action initializer) - { - _engineFunc = engineFunc; - _initializer = initializer; - } - - public IJavaScriptEngine GetEngine(TimeSpan? timeout = null) - { - IJavaScriptEngine engine = _engineFunc(); - _initializer?.Invoke(engine); - return engine; - } - - public void Dispose() - { - } - - public void RecycleEngine(IJavaScriptEngine engine) - { - throw new NotImplementedException(); - } - - public void RecycleAllEngines() - { - throw new NotImplementedException(); - } - } -} diff --git a/src/extensions/Statiq.Highlight/HighlightCode.cs b/src/extensions/Statiq.Highlight/HighlightCode.cs index bb712c51c..52fdb98f1 100644 --- a/src/extensions/Statiq.Highlight/HighlightCode.cs +++ b/src/extensions/Statiq.Highlight/HighlightCode.cs @@ -90,7 +90,7 @@ public HighlightCode WithAutoHighlightUnspecifiedLanguage(bool autoHighlight) /// protected override async Task> ExecuteContextAsync(IExecutionContext context) { - IJavaScriptEnginePool enginePool = context.GetJavaScriptEnginePool(x => + IJavaScriptEngine engine = context.GetJavaScriptEngine(x => { if (string.IsNullOrWhiteSpace(_highlightJsFile)) { @@ -101,7 +101,7 @@ protected override async Task> ExecuteContextAsync(IExecu x.ExecuteFile(_highlightJsFile); } }); - using (enginePool) + using (engine) { IEnumerable results = await context.Inputs.ParallelSelectAsync(async input => { @@ -125,7 +125,7 @@ protected override async Task> ExecuteContextAsync(IExecu try { - HighlightElement(enginePool, element); + HighlightElement(engine, element); highlighted = true; } catch (Exception innerEx) @@ -155,34 +155,31 @@ protected override async Task> ExecuteContextAsync(IExecu } } - internal static void HighlightElement(IJavaScriptEnginePool enginePool, AngleSharp.Dom.IElement element) + internal static void HighlightElement(IJavaScriptEngine engine, AngleSharp.Dom.IElement element) { - using (IJavaScriptEngine engine = enginePool.GetEngine()) - { - // Make sure to use TextContent, otherwise you'll get escaped html which highlight.js won't parse - engine.SetVariableValue("input", element.TextContent); + // Make sure to use TextContent, otherwise you'll get escaped html which highlight.js won't parse + engine.SetVariableValue("input", element.TextContent); - // Check if they specified a language in their code block - string language = element.ClassList.FirstOrDefault(i => i.StartsWith("language")); - if (language is object) - { - engine.SetVariableValue("language", language.Replace("language-", string.Empty)); - engine.Execute("result = hljs.highlight(language, input)"); - } - else + // Check if they specified a language in their code block + string language = element.ClassList.FirstOrDefault(i => i.StartsWith("language")); + if (language is object) + { + engine.SetVariableValue("language", language.Replace("language-", string.Empty)); + engine.Execute("result = hljs.highlight(language, input)"); + } + else + { + language = "(auto)"; // set this to auto in case there is an exception below + engine.Execute("result = hljs.highlightAuto(input)"); + string detectedLanguage = engine.Evaluate("result.language"); + if (!string.IsNullOrWhiteSpace(detectedLanguage)) { - language = "(auto)"; // set this to auto in case there is an exception below - engine.Execute("result = hljs.highlightAuto(input)"); - string detectedLanguage = engine.Evaluate("result.language"); - if (!string.IsNullOrWhiteSpace(detectedLanguage)) - { - element.ClassList.Add("language-" + detectedLanguage); - } + element.ClassList.Add("language-" + detectedLanguage); } - - element.ClassList.Add("hljs"); - element.InnerHtml = engine.Evaluate("result.value"); } + + element.ClassList.Add("hljs"); + element.InnerHtml = engine.Evaluate("result.value"); } } } \ No newline at end of file diff --git a/src/extensions/Statiq.Highlight/HighlightShortcode.cs b/src/extensions/Statiq.Highlight/HighlightShortcode.cs index 8b7f37a25..6ac9f2cc4 100644 --- a/src/extensions/Statiq.Highlight/HighlightShortcode.cs +++ b/src/extensions/Statiq.Highlight/HighlightShortcode.cs @@ -58,7 +58,7 @@ public override ShortcodeResult Execute(KeyValuePair[] args, str HighlightJsFile, AddPre); - using (IJavaScriptEnginePool enginePool = context.GetJavaScriptEnginePool(x => + using (IJavaScriptEngine engine = context.GetJavaScriptEngine(x => { if (dictionary.ContainsKey(HighlightJsFile)) { @@ -77,7 +77,7 @@ public override ShortcodeResult Execute(KeyValuePair[] args, str { element.SetAttribute("class", $"language-{dictionary.GetString("Language")}"); } - HighlightCode.HighlightElement(enginePool, element); + HighlightCode.HighlightElement(engine, element); if (dictionary.GetBool(AddPre) || (!dictionary.ContainsKey(AddPre) && content.Contains('\n'))) { return $"
{element.OuterHtml}
"; From b0a1d5c762962990e79f5d08bb2a502a19c14f16 Mon Sep 17 00:00:00 2001 From: Jason Summers <3616919+jasonsummers@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:12:56 +0000 Subject: [PATCH 2/2] refactor: ensure HighlightCode extension always uses a fresh JavaScript Engine. Under the previous Pooled model of JavaScript Engine provision, each call to the `HighlightCode.HighlightElement` function would retrieve a 'clean' JavaScript engine from the pool. Given that JavaScript Engines cannot be reset this change ensures that the previous behavior of consuming a fresh JavaScript Engine for every call to the `HighlightCode.HighlightElement` function is preserved. --- .../Statiq.Highlight/HighlightCode.cs | 101 +++++++++--------- .../Statiq.Highlight/HighlightShortcode.cs | 49 +++++---- 2 files changed, 78 insertions(+), 72 deletions(-) diff --git a/src/extensions/Statiq.Highlight/HighlightCode.cs b/src/extensions/Statiq.Highlight/HighlightCode.cs index 52fdb98f1..62bdc08e9 100644 --- a/src/extensions/Statiq.Highlight/HighlightCode.cs +++ b/src/extensions/Statiq.Highlight/HighlightCode.cs @@ -90,73 +90,76 @@ public HighlightCode WithAutoHighlightUnspecifiedLanguage(bool autoHighlight) /// protected override async Task> ExecuteContextAsync(IExecutionContext context) { - IJavaScriptEngine engine = context.GetJavaScriptEngine(x => + Func engineFunc = () => { - if (string.IsNullOrWhiteSpace(_highlightJsFile)) + return context.GetJavaScriptEngine(x => { - x.ExecuteResource("highlight.js", typeof(HighlightCode)); - } - else - { - x.ExecuteFile(_highlightJsFile); - } - }); - using (engine) + if (string.IsNullOrWhiteSpace(_highlightJsFile)) + { + x.ExecuteResource("highlight.js", typeof(HighlightCode)); + } + else + { + x.ExecuteFile(_highlightJsFile); + } + }); + }; + + IEnumerable results = await context.Inputs.ParallelSelectAsync(async input => { - IEnumerable results = await context.Inputs.ParallelSelectAsync(async input => + try { - try + IHtmlDocument htmlDocument = await input.ParseHtmlAsync(); + bool highlighted = false; + foreach (AngleSharp.Dom.IElement element in htmlDocument.QuerySelectorAll(_codeQuerySelector)) { - IHtmlDocument htmlDocument = await input.ParseHtmlAsync(); - bool highlighted = false; - foreach (AngleSharp.Dom.IElement element in htmlDocument.QuerySelectorAll(_codeQuerySelector)) + // Don't highlight anything that potentially is already highlighted + if (element.ClassList.Contains("hljs")) { - // Don't highlight anything that potentially is already highlighted - if (element.ClassList.Contains("hljs")) - { - continue; - } + continue; + } - // Skip highlighting if there is no language detected and auto highlight is disabled for unspecified languages - if (!element.ClassList.Any(c => c.StartsWith("language")) && !_autoHighlightUnspecifiedLanguage) - { - continue; - } + // Skip highlighting if there is no language detected and auto highlight is disabled for unspecified languages + if (!element.ClassList.Any(c => c.StartsWith("language")) && !_autoHighlightUnspecifiedLanguage) + { + continue; + } - try + try + { + HighlightElement(engineFunc, element); + highlighted = true; + } + catch (Exception innerEx) + { + if (innerEx.Message.Contains("Unknown language: ") && _warnOnMissingLanguage) { - HighlightElement(engine, element); - highlighted = true; + context.LogWarning($"Exception while highlighting source code: {innerEx.Message}"); } - catch (Exception innerEx) + else { - if (innerEx.Message.Contains("Unknown language: ") && _warnOnMissingLanguage) - { - context.LogWarning($"Exception while highlighting source code: {innerEx.Message}"); - } - else - { - context.LogInformation($"Exception while highlighting source code: {innerEx.Message}"); - } + context.LogInformation($"Exception while highlighting source code: {innerEx.Message}"); } } - - return highlighted ? input.Clone(context.GetContentProvider(htmlDocument)) : input; - } - catch (Exception ex) - { - context.LogWarning("Exception while highlighting source code for {0}: {1}", input.ToSafeDisplayString(), ex.Message); - return input; } - }); - // Materialize the results before disposing the JS engine - return results.ToList(); - } + return highlighted ? input.Clone(context.GetContentProvider(htmlDocument)) : input; + } + catch (Exception ex) + { + context.LogWarning("Exception while highlighting source code for {0}: {1}", input.ToSafeDisplayString(), ex.Message); + return input; + } + }); + + // Materialize the results before disposing the JS engine + return results.ToList(); } - internal static void HighlightElement(IJavaScriptEngine engine, AngleSharp.Dom.IElement element) + internal static void HighlightElement(Func engineFunc, AngleSharp.Dom.IElement element) { + using IJavaScriptEngine engine = engineFunc(); + // Make sure to use TextContent, otherwise you'll get escaped html which highlight.js won't parse engine.SetVariableValue("input", element.TextContent); diff --git a/src/extensions/Statiq.Highlight/HighlightShortcode.cs b/src/extensions/Statiq.Highlight/HighlightShortcode.cs index 6ac9f2cc4..404dde948 100644 --- a/src/extensions/Statiq.Highlight/HighlightShortcode.cs +++ b/src/extensions/Statiq.Highlight/HighlightShortcode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AngleSharp.Html.Parser; @@ -58,32 +59,34 @@ public override ShortcodeResult Execute(KeyValuePair[] args, str HighlightJsFile, AddPre); - using (IJavaScriptEngine engine = context.GetJavaScriptEngine(x => + IJavaScriptEngine EngineFunc() { - if (dictionary.ContainsKey(HighlightJsFile)) + return context.GetJavaScriptEngine(x => { - x.ExecuteFile(dictionary.GetString(HighlightJsFile)); - } - else - { - x.ExecuteResource("highlight.js", typeof(Statiq.Highlight.HighlightCode)); - } - })) + if (dictionary.ContainsKey(HighlightJsFile)) + { + x.ExecuteFile(dictionary.GetString(HighlightJsFile)); + } + else + { + x.ExecuteResource("highlight.js", typeof(Statiq.Highlight.HighlightCode)); + } + }); + } + + AngleSharp.Dom.IDocument htmlDocument = HtmlHelper.DefaultHtmlParser.ParseDocument(string.Empty); + AngleSharp.Dom.IElement element = htmlDocument.CreateElement(dictionary.GetString(Element, "code")); + element.InnerHtml = content.Trim(); + if (dictionary.ContainsKey(Language)) { - AngleSharp.Dom.IDocument htmlDocument = HtmlHelper.DefaultHtmlParser.ParseDocument(string.Empty); - AngleSharp.Dom.IElement element = htmlDocument.CreateElement(dictionary.GetString(Element, "code")); - element.InnerHtml = content.Trim(); - if (dictionary.ContainsKey(Language)) - { - element.SetAttribute("class", $"language-{dictionary.GetString("Language")}"); - } - HighlightCode.HighlightElement(engine, element); - if (dictionary.GetBool(AddPre) || (!dictionary.ContainsKey(AddPre) && content.Contains('\n'))) - { - return $"
{element.OuterHtml}
"; - } - return element.OuterHtml; + element.SetAttribute("class", $"language-{dictionary.GetString("Language")}"); + } + HighlightCode.HighlightElement(EngineFunc, element); + if (dictionary.GetBool(AddPre) || (!dictionary.ContainsKey(AddPre) && content.Contains('\n'))) + { + return $"
{element.OuterHtml}
"; } + return element.OuterHtml; } } } \ No newline at end of file