diff --git a/build.sbt b/build.sbt index 94b8d3f3f..2bb0256e9 100644 --- a/build.sbt +++ b/build.sbt @@ -2,9 +2,9 @@ import sbtcrossproject.CrossPlugin.autoImport.CrossType import sbtcrossproject.CrossProject import org.scalajs.sbtplugin.ScalaJSCrossVersion -val nlDependencyVersion = "7.0.0-beta2-8cd3e65" +val nlDependencyVersion = sys.env.getOrElse("NL_DEPENDENCY_VERSION", "7.0.0-beta2-8cd3e65") -val parserJsDependencyVersion = "0.4.0-8cd3e65" +val parserJsDependencyVersion = sys.env.getOrElse("PARSER_JS_DEPENDENCY_VERSION", "0.4.0-8cd3e65") val scalazVersion = "7.2.36" diff --git a/compiler/js/src/main/scala/BrowserCompiler.scala b/compiler/js/src/main/scala/BrowserCompiler.scala index 0fb50498c..9a70ffe53 100644 --- a/compiler/js/src/main/scala/BrowserCompiler.scala +++ b/compiler/js/src/main/scala/BrowserCompiler.scala @@ -14,6 +14,8 @@ import json.TortoiseJson._ import json.WidgetToJson import org.nlogo.core.{ CompilerException, Plot } +import org.nlogo.parse.FrontEnd + import org.nlogo.tortoise.compiler.xml.TortoiseModelLoader import org.nlogo.parse.CompilerUtilities @@ -30,6 +32,7 @@ import scalaz.std.list._ import scalaz.Scalaz.ToValidationOps import scalaz.Validation.FlatMap.ValidationFlatMapRequested + // scalastyle:off number.of.methods @JSExportTopLevel("BrowserCompiler") class BrowserCompiler { @@ -220,6 +223,23 @@ class BrowserCompiler { } + @JSExport + def listExtensions(source: String): NativeJson = { + val extensions: Seq[(String, Option[String])] = FrontEnd.findAllExtensions(source); + val json = JsArray( + extensions.map { + case (name, url) => + JsObject( + ListMap("name" -> JsString(name), "url" -> (url match { + case Some(u) => JsString(u) + case None => JsNull + })) + ) + } + ) + JsonLibrary.toNative(json) + } + @JSExport def listProcedures(): NativeJson = { diff --git a/compiler/js/src/main/scala/NLWExtensionsLoader.scala b/compiler/js/src/main/scala/NLWExtensionsLoader.scala new file mode 100644 index 000000000..e4748f220 --- /dev/null +++ b/compiler/js/src/main/scala/NLWExtensionsLoader.scala @@ -0,0 +1,39 @@ +// This file is related to the NetLogo Web (NLW) project +// Please see org:NetLogo/Galapagos/app/assets/javascript/beak/nlw-extensions-loader.js +// for more details. This change was introduced in https://github.com/NetLogo/NetLogo/pull/2508 +// Omar Ibrahim, 2025 +package org.nlogo.tortoise.compiler + +import scala.scalajs.js +import scala.scalajs.js.annotation.JSGlobal +import play.api.libs.json.{ JsValue, Json } + +// window.NLWExtensionsLoader is a global object and is +// gauranteed to be available before +@js.native +@JSGlobal("NLWExtensionsLoader") +object JSNLWExtensionsLoader extends js.Object { + def getPrimitivesFromURL(url: String): js.UndefOr[js.Object] = js.native + def appendURLProtocol(url: String): String = js.native + def validateURL(url: String): Boolean = js.native +} + +object NLWExtensionsLoader { + def getPrimitivesFromURL(url: String): Option[JsValue] = { + val result = JSNLWExtensionsLoader.getPrimitivesFromURL(url) + if (result == null || result == js.undefined) { + None + } else { + val json = Json.parse(js.JSON.stringify(result)) + Some(json) + } + } + + def appendURLProtocol(url: String): String = { + JSNLWExtensionsLoader.appendURLProtocol(url) + } + + def validateURL(url: String): Boolean = { + JSNLWExtensionsLoader.validateURL(url) + } +} \ No newline at end of file diff --git a/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala new file mode 100644 index 000000000..fff2f6c25 --- /dev/null +++ b/compiler/jvm/src/main/scala/NLWExtensionsLoader.scala @@ -0,0 +1,22 @@ +// This file is related to the NetLogo Web (NLW) project +// Please see org:NetLogo/Galapagos/app/assets/javascript/beak/nlw-extensions-loader.js +// for more details. This change was introduced in https://github.com/NetLogo/NetLogo/pull/2508 +// This is not supported in the JVM compiler. +// Omar Ibrahim, 2025 +package org.nlogo.tortoise.compiler +import scala.annotation.unused + +import play.api.libs.json.JsValue + + +object NLWExtensionsLoader { + def getPrimitivesFromURL(@unused url: String): Option[JsValue] = { + throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") + } + def appendURLProtocol(@unused url: String): String = { + throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") + } + def validateURL(@unused url: String): Boolean = { + throw new UnsupportedOperationException("NLWExtensionsLoader is not implemented in the JVM environment.") + } +} diff --git a/compiler/shared/src/main/scala/NLWExtensionManager.scala b/compiler/shared/src/main/scala/NLWExtensionManager.scala index fc7a9b628..aca54955d 100644 --- a/compiler/shared/src/main/scala/NLWExtensionManager.scala +++ b/compiler/shared/src/main/scala/NLWExtensionManager.scala @@ -59,6 +59,10 @@ object CreateExtension { def apply(json: String): Extension = { val jsExt = Json.parse(json) + fromJSON(jsExt) + } + + def fromJSON(jsExt: JsValue): Extension = { new Extension { override def getName: String = { jsExt("name").as[String] @@ -195,11 +199,28 @@ class NLWExtensionManager extends ExtensionManager { override def finishFullCompilation(): Unit = () override def importExtension(extName: String, errors: ErrorSource): Unit = { - val extension = extNameToExtMap.getOrElse(extName, failCompilation(s"No such extension: ${extName}")) + importExtension(extName, None, errors) + } + override def importExtension(extName: String, extURL: Option[String], errors: ErrorSource): Unit = { + val extension = extURL match { + case Some(url) => + if (!NLWExtensionsLoader.validateURL(url)) { + failCompilation(s"Invalid URL for extension: ${url}") + } + val extObj = NLWExtensionsLoader.getPrimitivesFromURL(url) + extObj match { + case Some(obj) => CreateExtension.fromJSON(obj) + case None => failCompilation(s"Could not load extension from URL: ${url}") + } + case None => extNameToExtMap.getOrElse(extName, failCompilation(s"No such extension: ${extName}")) + } val extPrimPairs = extension.getPrims.map(prim => (extension.getName, prim)) val shoutedPairs = extPrimPairs.map { case (extName, ExtensionPrim(prim, name)) => (s"$extName:$name".toUpperCase, prim) } primNameToPrimMap ++= shoutedPairs - importedExtensions.add(extName) + importedExtensions.add(extURL match { + case Some(url) => NLWExtensionsLoader.appendURLProtocol(url) + case None => extName + }) () } diff --git a/engine/src/main/coffee/extensions/all.coffee b/engine/src/main/coffee/extensions/all.coffee index ce25e72e8..5eebf9e61 100644 --- a/engine/src/main/coffee/extensions/all.coffee +++ b/engine/src/main/coffee/extensions/all.coffee @@ -14,6 +14,15 @@ module.exports = { if upperNames.includes(upperName) extensions[upperName] = extension ) + importedExtensions.filter(NLWExtensionsLoader.isURL).forEach( (url) -> + extensionModule = NLWExtensionsLoader.getExtensionModuleFromURL(url) + if extensionModule? + extension = extensionModule.init(workspace) + upperName = extension.name.toUpperCase() + extensions[upperName] = extension + else + console.warn "Extension at URL #{url} does not have an init function." + ) extensions porters: (importedExtensions...) -> @@ -26,6 +35,14 @@ module.exports = { if upperNames.includes(upperName) porters.push(extensionModule.porter) ) + importedExtensions.filter(NLWExtensionsLoader.isURL).forEach( (url) -> + extensionModule = NLWExtensionsLoader.getExtensionModuleFromURL(url) + if extensionModule? and extensionModule.porter? + upperName = extensionModule.porter.extensionName.toUpperCase() + porters.push(extensionModule.porter) + else + console.warn "Extension at URL #{url} does not have an init function." + ) porters }