From 43ff366ca4c6f0e23f19b079f0ef179abe07be54 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Wed, 26 Nov 2025 16:14:26 +0100 Subject: [PATCH] stable version: JML in description, multiple mastodon-tags supported --- manyfold/cli/manyfold.sh | 5 +- .../experience_updated/300-package_janusxr.rb | 146 ++++++++++++++ .../experience_updated/500-mastodon-post.rb | 44 ++-- manyfold/root/xrforge.rb | 190 +++++++----------- .../usr/src/app/public/assets/xrforge.css | 11 + 5 files changed, 251 insertions(+), 145 deletions(-) diff --git a/manyfold/cli/manyfold.sh b/manyfold/cli/manyfold.sh index 9091091..0606574 100755 --- a/manyfold/cli/manyfold.sh +++ b/manyfold/cli/manyfold.sh @@ -127,7 +127,8 @@ set_upload_path(){ test -d $UPLOAD_PATH || mkdir $UPLOAD_PATH add_lib_to_db $UPLOAD_PATH id=$(sqlite3 $db "select id from libraries where path = '$UPLOAD_PATH';") - sqlite3 $db "INSERT INTO settings VALUES(6,'default_library',replace('--- $id\n','\n',char(10); + set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))" + sqlite3 $db "INSERT INTO settings VALUES(6,'default_library',$id); INSERT INTO sqlite_sequence VALUES('settings',6);" } @@ -258,8 +259,8 @@ init_database(){ set_global site_tagline "'$TAGLINE'" set_global model_tags_auto_tag_new "replace('--- \"\"\\n','\\n',char(10))" set_global model_path_template "replace('--- \"{creator}/{modelId} \"\\n','\\n',char(10))" + set_upload_path #set_global about "$ABOUT" - set_upload_path & get_xrfragment_assets mount_dir BOOT_SCAN=1 scan_libraries & diff --git a/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb b/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb index e69de29..a7d35df 100755 --- a/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb +++ b/manyfold/root/hook.d/experience_updated/300-package_janusxr.rb @@ -0,0 +1,146 @@ +#!/usr/bin/env ruby + +require 'json' +require_relative './../../xrforge.rb' + +# Check if a filename is provided +if ARGV.length != 1 + puts "Usage: #{$0} " + exit 1 +end + +filename = ARGV[0] +JMLHeuristic = /.*?<\/fireboxroom>/im + +begin + + # dont run for each file-update + if ! filename.end_with?("datapackage.json") + exit 0 + end + + # Change the directory + dir = File.dirname(filename) + Dir.chdir( File.dirname(filename) ) + # Read and parse the JSON file + data = JSON.parse( File.read( "datapackage.json" ) ) + + #if data['keywords'].empty? || data['keywords'].include?('janusxr') + + logfile = File.join( File.dirname(filename), ".xrforge/log.txt" ) + + XRForge.log("✅ starting build janusXR XR scene", logfile) + + # Extract the desired field (assuming the field is named 'model_file') + thumb_file = data['image'] + + XRForge.log("✅ thumbnail sidecar-file '#{thumb_file}' detected", logfile) + + # Get the base name of the thumbnail file without its extension + base_name = File.basename(thumb_file, File.extname(thumb_file)) + + model_file = nil # Initialize model_file to nil + + # Loop over the list of extensions + XRForge::MODEL_EXT.each do |ext| + # Construct the filename with the current extension + filename = "#{base_name}#{ext}" + + # Check if the file exists + if File.exist?(filename) + XRForge.log("✅ 3D file '#{filename}' detected", logfile) + model_file = "#{dir.gsub("/mnt/","")}/#{filename}" # Store the found filename + break # Stop the loop once a file is found + else + # Log a message for the file that was not found, but don't stop + XRForge.log("⚠️ 3D file '#{filename}' not detected", logfile) + end + end + + # Check if a model file was found after the loop + if model_file + XRForge.log("✅ Final model file: '#{model_file}'", logfile) + else + XRForge.log("❌ No suitable 3D file found for XR Fragments- / JanusXR-compatible experience", logfile) + end + + # Get the value of the environment variable FEDERATE_DRIVE_HOST + federate_drive_host = ENV['FEDERATE_DRIVE_HOST'] + + autogenerate = true + + if data['description'] && data['description'].match(JMLHeuristic) + if data['description'].match(/autogenerate=['"]false['"]/) + XRForge.log("✅ autogenerate='false' found in JML..keeping this JML") + autogenerate = false + jmlMatch = data['description'].match(JMLHeuristic) + jml = jmlMatch[0] + end + end + if autogenerate + jml = <<~JML + + + + + + + + + + + JML + + data['description'] = data['description'] ? data['description'] : "your description here\n" + data['description'] = <<~DESCRIPTION#{data['description']} + + + + + + + + +#{jml} + DESCRIPTION + File.write("datapackage.json", JSON.pretty_generate(data) ) + end + + html = <<~HTML + + + + janusxr room + + + + + #{jml} + + + + + + HTML + + File.write('.xrforge/janusxr.html', html) + File.write('.xrforge/scene.jml', jml) + + XRForge.log("✅ generated scene.jml", logfile) + XRForge.log("✅ generated janusxr.html", logfile) + XRForge.log(" ", logfile) + + # tag it! + if ! data['keywords'].include?('janusxr') + data['keywords'].push('janusxr') + File.write("datapackage.json", JSON.pretty_generate(data) ) + end + +rescue Errno::ENOENT + puts "File #{filename} not found" +rescue JSON::ParserError + puts "Error parsing JSON from #{filename}" +rescue => e + puts "An error occurred: #{e.message}" +end + diff --git a/manyfold/root/hook.d/experience_updated/500-mastodon-post.rb b/manyfold/root/hook.d/experience_updated/500-mastodon-post.rb index d09463b..5990376 100755 --- a/manyfold/root/hook.d/experience_updated/500-mastodon-post.rb +++ b/manyfold/root/hook.d/experience_updated/500-mastodon-post.rb @@ -19,28 +19,11 @@ if ! filename.end_with?("datapackage.json") exit 0 end -begin - # Change the directory - dir = File.dirname(filename) - Dir.chdir( dir ) - # Read and parse the JSON file - data = JSON.parse( File.read( "datapackage.json" ) ) - APHandle = false +def generate(aphandle,filename,dir,data,logfile) - data['keywords'].any? do |tag| - if tag.match?(/@.*@.*\./) # scan for activitypub handle (@foo@mastodon.online e.g.) - APHandle = tag - end - end + XRForge.log("✅ starting mastodon-post for #{aphandle}", logfile) - if ! APHandle # nothing to do - exit 0 - end - - logfile = File.join( File.dirname(filename), ".xrforge/log.txt" ) - XRForge.log("✅ starting Mastodon post2image", logfile) - - parts = APHandle.split("@") + parts = aphandle.split("@") server = parts[2].sub(/@/,"") rssUrl = "https://#{server}/@#{parts[1]}.rss" XRForge.log("✅ checking #{rssUrl}", logfile) @@ -66,14 +49,29 @@ begin puts description # look for first image - MEDIA_REGEX = / attribute), hash nesting, and arrays + # (which create multiple sibling elements of the same name). # - # @param data [Hash, Array] The current portion of the data structure. - # @param parent_rexml [REXML::Element] The parent REXML element to attach children/attributes to. - def self.json2xml_recursive(data, parent_rexml) - return if data.nil? + # @param data [Hash, Array, String] The data fragment. + # @param p [REXML::Element] The parent REXML element. + def self.j2x_rec(data, p) + # Check if data is a Hash (for attributes and children) + if data.is_a?(Hash) + data.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_rec(v, p.add_element(k)) } + # Check if data is an Array (for repeated elements) + elsif data.is_a?(Array) + # The parent in this context (e.g., ) is already created. For each array item, + # we need to create a new element with the same name as the current parent's *name* + # and attach it to the parent's *parent*. + # This requires looking up the parent's parent, which breaks terseness. + # We must use a simpler recursive structure for compactness. - # If the current value is an Array, we iterate over its items. - # Each item is expected to define a new child element or a structure to recurse on. - if data.is_a?(Array) - data.each do |item| - # Recursively process each item in the array against the *current* parent. - json2xml_recursive(item, parent_rexml) - end - return - end - - # The data should be a Hash containing key-value pairs representing elements or attributes. - data.each do |key, value| - if key.start_with?('@') - # 1. Attribute: set on the parent REXML element (removing the leading '@') - parent_rexml.attributes[key[1..-1]] = value.to_s - elsif value.is_a?(Hash) || value.is_a?(Array) - # 2. Child Element: create a new element and recurse - new_element = parent_rexml.add_element(key) - json2xml_recursive(value, new_element) - else - # 3. Text Content: create a new element with primitive content (e.g., strings, numbers) - new_element = parent_rexml.add_element(key) - new_element.text = value.to_s - end + # We assume array structure requires us to create a new element named by the + # parent, and assign attributes/children recursively. + data.each { |v| j2x_rec(v, p) } # Recursive call to continue processing children + # Handle text content + else + p.text = data.to_s end end - # Converts a Ruby Hash structure (JSON-like) into a compressed XML string. + # The entry point for converting Ruby Hash to XML string. # # @param data [Hash] The root hash structure. - # @return [String] The resulting XML string. + # @return [String] The resulting compact XML string. def self.json2xml(data) - # Start with an empty REXML document - doc = REXML::Document.new - - # Data must have a single root element (e.g., 'fireboxroom') - data.each do |root_key, root_value| - root_element = REXML::Element.new(root_key) - doc.add_element(root_element) - json2xml_recursive(root_value, root_element) - # Since the input structure only defines one root key, we break after the first one. - break - end - - # Convert REXML document to XML string without the XML declaration - xml_output = "" - formatter = REXML::Formatters::Pretty.new(2) # indentlevel 2 - - # Write only the root element, ignoring the XML declaration - doc.root.write(xml_output, 0) - xml_output.strip - end - - - # ============================================================================== - # 2. XML to JSON (Ruby Hash) Conversion - # ============================================================================== - - # Recursively converts an REXML element into a Hash structure. - # Elements that appear multiple times become an Array in the Hash. - # Attributes are added with an '@' prefix. - # - # @param element [REXML::Element] The XML element to convert. - # @return [Hash, String] The resulting hash or a text string if the element has no children. - def self.xml2json_recursive(element) - hash = {} - - # 1. Handle Attributes - element.attributes.each do |name, value| - hash["@#{name}"] = value - end - - # 2. Handle Text Content - # If the element has text content but no children (e.g., 10.99), - # return the text directly unless the hash already contains attributes. - if element.has_text? - text_content = element.get_text.value.strip - if !text_content.empty? - if element.elements.empty? && hash.empty? - # If it's pure text (no attributes, no children), return string value - return text_content - elsif element.elements.empty? - # If it has attributes but no children, the text is the '_content' - hash["#text"] = text_content - end - end - end - - # 3. Handle Child Elements - element.elements.each do |child| - child_key = child.name - child_hash = xml2json_recursive(child) - - if hash.key?(child_key) - # If key already exists, convert to array or append to array (standard convention) - if hash[child_key].is_a?(Array) - hash[child_key] << child_hash - else - hash[child_key] = [hash[child_key], child_hash] - end + # Corrected recursive logic for arrays that are values (e.g., "obj" => [..]) + j2x_map = lambda do |d, p_name, p| + if d.is_a?(Hash) + d.each { |k, v| k.start_with?('@') ? p.attributes[k[1..-1]] = v.to_s : j2x_map.call(v, k, p.add_element(k)) } + elsif d.is_a?(Array) + # If the value is an array, we iterate, create a sibling element for each, and recurse + d.each { |v| j2x_map.call(v, p.name, p.parent.add_element(p.name)) } + p.remove # Remove the placeholder element created before hitting the array else - # New key - hash[child_key] = child_hash + p.text = d.to_s end end - - # Special handling to match the user's specific array format for the 'assets' node. - # This makes the output match the input test case, but is non-standard for general XML-JSON mappers. - if hash.key?("assets") && hash["assets"].is_a?(Hash) && hash["assets"].key?("asset") - asset_content = hash["assets"].delete("asset") - # Force the structure to be: [ {"asset" => asset_content} ] - hash["assets"] = [ {"asset" => asset_content} ] - end - hash + # Initialize Document and process the single root key + doc = REXML::Document.new + data.each { |k, v| j2x_map.call(v, k, doc.add_element(k)) } + + # Output with 2-space indentation + o = ""; f = REXML::Formatters::Default.new; f.indentation = 2; f.write(doc.root, o); o.strip end - # Converts an XML string into a corresponding Ruby Hash (JSON-like) structure. + # ============================================================================== + # 2. XML String to JSON (Ruby Hash) + # ============================================================================== + + # Internal recursive helper for converting REXML Element to Hash structure. + # Handles attribute conversion (attribute -> @key), text, and array creation for siblings. + # + # @param e [REXML::Element] The XML element. + # @return [Hash, String] The resulting hash or simple text value. + def self.x2j_rec(e) + h = {}; e.attributes.each { |k, v| h["@#{k}"] = v } # Attributes first + + e.elements.each do |c| # Iterate children + v = x2j_rec(c) + if h.key?(c.name) + h[c.name] = [h[c.name]] unless h[c.name].is_a?(Array) + h[c.name] << v + else + h[c.name] = v + end + end + + # Check for text content if no children were processed + t = e.get_text.to_s.strip; return t if t && !t.empty? && h.empty?; return h + end + + # The entry point for converting XML string to Ruby Hash. # # @param xmlstr [String] The XML string. # @return [Hash] The resulting Ruby hash structure. def self.xml2json(xmlstr) doc = REXML::Document.new(xmlstr) - root_element = doc.root - return {} unless root_element - - # The final output is a hash containing the single root element and its content. - { root_element.name => xml2json_recursive(root_element) } - end + root = doc.root + { root.name => x2j_rec(root) } + end end diff --git a/manyfold/usr/src/app/public/assets/xrforge.css b/manyfold/usr/src/app/public/assets/xrforge.css index a4d0f09..6934af0 100644 --- a/manyfold/usr/src/app/public/assets/xrforge.css +++ b/manyfold/usr/src/app/public/assets/xrforge.css @@ -21,3 +21,14 @@ input[type="checkbox"]:checked + .hidden-tooltip { display: block; } +#model_notes { + min-height: 50vh; + font-family: monospace; + font-size: 13px; + color: #CCF; + background: #000; +} + +.col-form-label{ + width:111px; +}