Module:MwJson: Difference between revisions

30,066 bytes added ,  Thursday at 04:47
m
Protected "Module:MwJson": Protected as read-only import via Page Exchange extension ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite)) [cascading]
(Install package: OSW Core)
 
m (Protected "Module:MwJson": Protected as read-only import via Page Exchange extension ([Edit=Allow only administrators] (indefinite) [Move=Allow only administrators] (indefinite)) [cascading])
 
(2 intermediate revisions by the same user not shown)
Line 1: Line 1:
-- mw.logObject(p.processJsondata({jsondata=p.loadJson({title="Item:OSW7d7193567ea14e4e89b74de88983b718", slot="jsondata"}).json, debug=true, mode="header"}))
local lustache = require("Module:Lustache")
local lustache = require("Module:Lustache")


Line 5: Line 7:
p.keys = { --jsonschema / json-ld keys
p.keys = { --jsonschema / json-ld keys
category='type',  
category='type',  
category_pseudoproperty='Category', -- Property:Category
subcategory='subclass_of',
subcategory='subclass_of',
schema_type='schema_type',
schema_type='schema_type',
Line 17: Line 20:
description='description',
description='description',
text='text',
text='text',
smw_quantity_property='x-smw-quantity-property',
debug='_debug'
debug='_debug'
}  
}
p.slots = { --slot names
p.slots = { --slot names
main='main',
jsondata='jsondata',  
jsondata='jsondata',  
jsonschema='jsonschema',  
jsonschema='jsonschema',  
Line 31: Line 36:
query='query'
query='query'
}
}
p.cache = {}


--loads json from a wiki page
--loads json from a wiki page
Line 37: Line 44:
function p.loadJson(args)
function p.loadJson(args)
local page_title = p.defaultArg(args.title, "JsonSchema:Entity") --for testing
local page_title = p.defaultArg(args.title, "JsonSchema:Entity") --for testing
local slot = p.defaultArg(args.slot, nil)
local slot = p.defaultArg(args.slot, 'main')
local debug = p.defaultArg(args.debug, nil)
local debug = p.defaultArg(args.debug, nil)
local msg = ""
local msg = ""
Line 43: Line 50:
local json = {}
local json = {}
if (slot == nil) then
if p.cache[page_title] ~= nil then
if p.cache[page_title][slot] ~= nil then
if (debug) then msg = msg .. "Fetch slot " .. p.slots.jsondata .. " of page " .. page_title .. " from cache <br>" end
json = p.cache[page_title][slot]
return {json=json, debug_msg=msg}
end
else p.cache[page_title] = {}
end
if (slot == 'main') then
--json = mw.loadJsonData( "JsonSchema:Entity" ) --requires MediaWiki 1.39
--json = mw.loadJsonData( "JsonSchema:Entity" ) --requires MediaWiki 1.39
local page = mw.title.makeTitle(p.splitString(page_title, ':')[1], p.splitString(page_title, ':')[2])
local page = mw.title.makeTitle(p.splitString(page_title, ':')[1], p.splitString(page_title, ':')[2])
Line 49: Line 65:
if (text ~= nil) then json = mw.text.jsonDecode(text) end
if (text ~= nil) then json = mw.text.jsonDecode(text) end
else
else
if (debug) then msg = msg .. "Fetch slot " .. p.slots.jsondata .. " from page " .. title .. "<br>" end
if (debug) then msg = msg .. "Fetch slot " .. p.slots.jsondata .. " of page " .. page_title .. "<br>" end
local text = mw.slots.slotContent( slot , page_title )
local text = mw.slots.slotContent( slot , page_title )
if (text ~= nil) then json = mw.text.jsonDecode(text) end
if (text ~= nil) then json = mw.text.jsonDecode(text) end
end
end
local generated_content = json["$defs"] and json["$defs"]["generated"]
if generated_content then
    -- Check if "$ref": "#/$defs/generated" exists directly in the table
    if json["$ref"] == "#/$defs/generated" then
        json = p.tableMerge(p.copy(generated_content), json)
        json["$ref"] = nil -- Remove the reference after merging
    end
    -- Check if "$ref": "#/$defs/generated" is contained in "allOf"
    if json["allOf"] then
        for _, item in ipairs(json["allOf"]) do
            if item["$ref"] == "#/$defs/generated" then
           
            -- Remove the reference after merging
                for i, v in ipairs(json["allOf"]) do
                    if v["$ref"] == "#/$defs/generated" then
                        table.remove(json["allOf"], i)
                        break
                    end
                end
                json = p.tableMerge(p.copy(generated_content), json)
               
                break
            end
        end
    end
    json["$defs"]["generated"] = nil
end
--mw.logObject(json)
--mw.logObject(json)
p.cache[page_title][slot] = json


return {json=json, debug_msg=msg}
return {json=json, debug_msg=msg}
end
end


-- test: mw.logObject(p.walkJsonSchema({jsonschema=p.loadJson({title="Category:Hardware", slot="jsonschema"}).json, debug=true}).jsonschema)
-- test: mw.logObject(p.walkJsonSchema({jsonschema=p.loadJson({title="Category:Hardware", slot="jsonschema"}).json, debug=true}).jsonschema)
Line 67: Line 116:
local mode = p.defaultArg(args.mode, p.mode.header)
local mode = p.defaultArg(args.mode, p.mode.header)
--local merged_jsonschema = p.defaultArg(args.merged_jsonschema, {})
--local merged_jsonschema = p.defaultArg(args.merged_jsonschema, {})
local template = p.defaultArg(args.template, nil)
local templates = p.defaultArg(args.templates, {})
local templates = p.defaultArg(args.templates, {})
local recursive = p.defaultArg(args.recursive, true)
local recursive = p.defaultArg(args.recursive, true)
Line 78: Line 128:
if (mode == p.mode.header) then category_template_slot = p.slots.header_template end
if (mode == p.mode.header) then category_template_slot = p.slots.header_template end
if (categories == nil) then categories = p.getCategories({jsonschema=jsonschema, includeNamespace=true}).categories end
if (categories == nil) then categories = p.getCategories({jsonschema=jsonschema, includeNamespace=true, includeSchemas=true}).categories end
if (type(categories) ~= 'table') then categories = {categories} end
if (type(categories) ~= 'table') then categories = {categories} end
if (debug) then msg = msg .. "Supercategories: " .. mw.dumpObject(categories) .. "\n<br>" end
if (debug) then msg = msg .. "Supercategories: " .. mw.dumpObject(categories) .. "\n<br>" end
Line 85: Line 135:
--mw.logObject("Visit " .. category)
--mw.logObject("Visit " .. category)
if (debug) then msg = msg .. "Fetch slot " .. p.slots.jsonschema .. " from page " .. category .. "\n<br>" end
if (debug) then msg = msg .. "Fetch slot " .. p.slots.jsonschema .. " from page " .. category .. "\n<br>" end
local super_jsonschema_str = mw.slots.slotContent( p.slots.jsonschema , category )
local super_jsonschema = nil
if (super_jsonschema_str ~= nil) then
if p.splitString(category, ':')[1] == "JsonSchema" then super_jsonschema = p.loadJson({title=category, slot=p.slots.main}).json
super_jsonschema = mw.text.jsonDecode( super_jsonschema_str )
else super_jsonschema = p.loadJson({title=category, slot=p.slots.jsonschema}).json end
if (super_jsonschema ~= nil) then
if (recursive) then
if (recursive) then
local res = p.walkJsonSchema({jsonschema=super_jsonschema, jsonschemas=jsonschemas, templates=templates, mode=mode, visited=visited, root=false})
local res = p.walkJsonSchema({jsonschema=super_jsonschema, jsonschemas=jsonschemas, templates=templates, mode=mode, visited=visited, root=false})
Line 96: Line 147:
--mw.logObject("Store " .. category)
--mw.logObject("Store " .. category)
table.insert(visited, category)
table.insert(visited, category)
jsonschemas[category] = mw.text.jsonDecode( super_jsonschema_str ) --keep a copy of the schema, super_jsonschema passed by references gets modified
jsonschemas[category] = p.copy( super_jsonschema ) --keep a copy of the schema, super_jsonschema passed by references gets modified
--jsonschema = p.tableMerge(jsonschema, super_jsonschema) --merge superschema is done by the caller
--jsonschema = p.tableMerge(jsonschema, super_jsonschema) --merge superschema is done by the caller
end
end
Line 104: Line 156:
end
end
end
end
-- Process propertyOrder based on nesting level
if (root) then
if (root) then
jsonschema = {}
local visited_properties = {}
-- Iterate through visited schemas and adjust propertyOrder
for i, category in ipairs(visited) do
local schema = jsonschemas[category]
-- Calculate level: last visited (most specific) has level=1, first (base) has highest level
local level = #visited - i + 1
if schema and schema.properties then
for property, prop_data in pairs(schema.properties) do
-- Set default propertyOrder if not set and property not visited before
if not prop_data.propertyOrder and not visited_properties[property] then
prop_data.propertyOrder = 1000
end
if prop_data.propertyOrder then
local order = prop_data.propertyOrder
if order < 0 then
-- Absolute value - keep as is (currently not ranked correctly)
prop_data.propertyOrder = order
elseif order <= 1000 then
-- Insert on top, rank higher levels before lower levels
-- Default value is 1000, so we shift -2*1000 per level
prop_data.propertyOrder = (1000 * 1000 - level * 2000) + order
else -- order > 1000
-- Insert at bottom, rank higher levels after lower levels
-- Default value is 1000, so we shift +2*1000 per level
prop_data.propertyOrder = (1000 * 1000 + level * 2000) + order
end
end
-- Mark property as visited
visited_properties[property] = true
end
end
end
-- Merge all schemas after adjusting propertyOrder
for i, category in ipairs(visited) do
for i, category in ipairs(visited) do
jsonschema = p.tableMerge(jsonschema, jsonschemas[category]) --merge all schemas
--merge all schemas. we need to make a copy here, otherwise jsonschemas["Category:Entity"] contains the merged schema
jsonschema = p.copy(p.tableMerge(jsonschema, jsonschemas[category]))
end
end
end
end
Line 134: Line 226:
local msg = ""
local msg = ""
local res = p.defaultArg(args.jsondata, "")
local res = p.defaultArg(args.jsondata, "")
local root = p.defaultArg(args.root, true) -- first entry into recursion
local debug = p.defaultArg(args.debug, false)
for k,v in pairs(jsondata) do
for k,v in pairs(jsondata) do
Line 149: Line 243:
-- mustache can handle objects and array to we can parse it directly
-- mustache can handle objects and array to we can parse it directly
-- todo: handle nested templates
-- todo: handle nested templates
if (debug) then msg = msg .. "Parse mustache template " .. eval_template.value .. " with params " .. mw.dumpObject( {[k]=v} ) .. "\n<br>" end
local template_param = {[k]=v}
jsondata[k] = lustache:render(eval_template.value, {[k]=v})
if (eval_template.root_key == false) then template_param = v end
if (debug) then msg = msg .. "Parse mustache template " .. eval_template.value .. " with params " .. mw.dumpObject( template_param ) .. "\n<br>" end
jsondata[k] = lustache:render(eval_template.value, template_param, p.tableMerge({self=eval_template.value}, eval_template.partials)) -- render with self as registered partial for recursion
if (eval_template.type == "mustache-wikitext") then  
if (eval_template.type == "mustache-wikitext") then  
jsondata[k] = frame:preprocess( jsondata[k] )
jsondata[k] = frame:preprocess( jsondata[k] )
end
end
elseif type(v) == 'table' then  
elseif type(v) == 'table' then  
if (v[1] == nil) then --key value array = object/dict
if (p.tableLength(v) > 0 and v[1] == nil) then --key value array = object/dict
local sub_res = p.expandEmbeddedTemplates({frame=frame, jsondata=v, jsonschema=p.defaultArgPath(jsonschema, {"properties", k}, {}), template=eval_template, mode=mode, stringify_arrays=stringify_arrays})
local sub_res = p.expandEmbeddedTemplates({frame=frame, jsondata=v, jsonschema=p.defaultArgPath(jsonschema, {"properties", k}, {}), template=eval_template, mode=mode, stringify_arrays=stringify_arrays, root=false, debug=debug})
msg = msg .. sub_res.debug_msg
msg = msg .. sub_res.debug_msg
jsondata[k] = sub_res.res
jsondata[k] = sub_res.res
Line 176: Line 272:


if type(e) == 'table' then
if type(e) == 'table' then
local sub_res = p.expandEmbeddedTemplates({frame=frame, jsondata=e, jsonschema=p.defaultArgPath(jsonschema, {"properties", k, "items"}, {}), template=eval_template, mode=mode, stringify_arrays=stringify_arrays})
local sub_res = p.expandEmbeddedTemplates({frame=frame, jsondata=e, jsonschema=p.defaultArgPath(jsonschema, {"properties", k, "items"}, {}), template=eval_template, mode=mode, stringify_arrays=stringify_arrays, root=false, debug=debug})
msg = msg .. sub_res.debug_msg
msg = msg .. sub_res.debug_msg
if (type(sub_res.res) == 'table') then  
if (type(sub_res.res) == 'table') then  
Line 185: Line 281:
end
end
else
else
if (eval_template ~= nil and eval_template["value"] ~= nil) then
if (eval_template ~= nil and eval_template.value ~= nil) then
--evaluate single array item string as json {"self": "<value>", ".": "<value>"}
local sub_res = p.expandEmbeddedTemplates({frame=frame, jsondata={["self"]=e,["."]=e}, jsonschema=p.defaultArgPath(jsonschema, {"properties", k, "items"}, {}), template=eval_template, mode=mode, stringify_arrays=stringify_arrays})
--evaluate single array item string as json {"self": "<value>", ".": "<value>"} => does not work since jsondata is an object
mw.logObject(sub_res)
--e = p.expandEmbeddedTemplates({frame=frame, jsondata={["self"]=e,["."]=e}, jsonschema=p.defaultArgPath(jsonschema, {"properties", k, "items"}, {}), template=eval_template, mode=mode, stringify_arrays=stringify_arrays, root=false, debug=debug})
e = sub_res.res
v[i] = e
 
if (eval_template.type == "mustache" or eval_template.type == "mustache-wikitext") then
if (debug) then msg = msg .. "Parse mustache template " .. eval_template.value .. " with params " .. mw.dumpObject( e ) .. "\n<br>" end
-- {{.}} in the template will be the value of e
e = lustache:render(eval_template.value, e, p.tableMerge({self=eval_template.value}, eval_template.partials)) -- render with self as registered partial for recursion
end
if (eval_template.type == "mustache-wikitext") then --or eval_template.type == "wikitext") then
if (debug) then msg = msg .. "Parse wikitext template " .. e .. " with params " .. mw.dumpObject( e ) .. "\n<br>" end
e = frame:preprocess( e )
end
v[i] = e -- update array
end
end
if (stringify_arrays) then string_list = string_list .. e .. ";" end
if (stringify_arrays) then string_list = string_list .. e .. ";" end
Line 201: Line 307:
if (template == nil) then  
if (template == nil and root == false) then -- don't stringify root json objects
local templates = jsondata[p.keys.template]
local templates = jsondata[p.keys.template]
if (templates == nil) then templates = p.defaultArg(jsonschema[p.keys.template], {}) end
if (templates == nil) then templates = p.defaultArg(jsonschema[p.keys.template], {}) end
Line 213: Line 319:
end
end
if template ~= nil then
if (template ~= nil and root == false) then -- don't stringify root json objects
if (template.type == "wikitext") then
if (template.type == "wikitext") then
for k,v in pairs(jsondata) do
for k,v in pairs(jsondata) do
Line 262: Line 368:
local schema_res = p.walkJsonSchema({jsonschema=jsonschema, categories=categories, mode=mode, recursive=recursive, debug=debug})
local schema_res = p.walkJsonSchema({jsonschema=jsonschema, categories=categories, mode=mode, recursive=recursive, debug=debug})
jsonschema = p.expandJsonRef({json=schema_res.jsonschema, debug=debug}).json
local expand_res = p.expandJsonRef({json=schema_res.jsonschema, debug=debug})
--mw.logObject(jsonschema)
jsonschema = expand_res.json
--mw.log(mw.text.jsonEncode(jsonschema))
local display_label = p.defaultArgPath(jsondata, {p.keys.name}, "")
 
if (display_label == "" or (title.nsText ~= "Category" and title.nsText ~= "Property")) then
 
display_label = p.defaultArgPath(jsondata, {p.keys.label, 1, p.keys.text}, "") --prefere label for all non-category and non-property pages
end
local jsonld = p.copy(jsondata)
local jsonld = p.copy(jsondata)
local json_data_store = p.copy(jsondata)
local json_data_store = p.copy(jsondata)
local json_data_render = p.copy(jsondata)
local json_data_render = p.copy(jsondata)
json_res_store = p.expandEmbeddedTemplates({jsonschema=jsonschema, jsondata=json_data_store, mode='store'})
json_res_store = p.expandEmbeddedTemplates({jsonschema=jsonschema, jsondata=json_data_store, mode='store', debug=debug})
msg = msg .. json_res_store.debug_msg
msg = msg .. json_res_store.debug_msg
--mw.log("JSONDATA STORE")
--mw.log("JSONDATA STORE")
Line 280: Line 383:
local smw_res = nil
local smw_res = nil
if (mode == p.mode.header) then
if (mode == p.mode.header) then
-- get the semantic properties by looking up the json keys in the json-ld context
smw_res = p.getSemanticProperties({jsonschema=jsonschema, jsondata=json_res_store.res, store=false, debug=debug})
smw_res = p.getSemanticProperties({jsonschema=jsonschema, jsondata=json_res_store.res, store=false, debug=debug})
-- store metadata where properties were defined / overridden
for i, category in ipairs(schema_res.visited) do
for k, v in pairs(p.defaultArgPath(schema_res.jsonschemas, {category, 'properties'}, {})) do --property section may not exisit
if smw_res.definitions[k] == nil then smw_res.definitions[k] = {} end
if smw_res.definitions[k]['defined_in'] == nil then smw_res.definitions[k]['defined_in'] = {} end
table.insert(smw_res.definitions[k]['defined_in'], category)
end
end
-- embed json-ld in resulting html for search engine discovery
jsonld["@context"] = smw_res.context
jsonld["@context"] = smw_res.context
jsonld["@type"] = p.tableMerge(p.tablefy(jsonschema.schema_type), p.tablefy(jsonld["@type"])) --
jsonld["@type"] = p.tableMerge(p.tablefy(jsonschema.schema_type), p.tablefy(jsonld["@type"])) --
Line 299: Line 415:
--mw.log("JSONDATA RENDER")
--mw.log("JSONDATA RENDER")
--mw.logObject(jsondata)
--mw.logObject(jsondata)
local renderMode = "tree"
if jsondata.__render_mode__ == "tree" then renderMode = "tree" end
if jsondata.__render_mode__ == "table" then renderMode = "table" end


local max_index = p.tableLength(schema_res.visited)
local max_index = p.tableLength(schema_res.visited)
for i, category in ipairs(schema_res.visited) do
for i, category in ipairs(schema_res.visited) do
if (mode == p.mode.footer) then category = schema_res.visited[max_index - i +1] end --reverse order for footer templates
if (mode == p.mode.footer) then category = schema_res.visited[max_index - i +1] end --reverse order for footer templates
local jsonschema = schema_res.jsonschemas[category]
local super_jsonschema = schema_res.jsonschemas[category]
local template = schema_res.templates[category]
local template = schema_res.templates[category]
local _details = nil
if (mode == p.mode.header and renderMode == "tree" and i == 1) then -- insert tree after root infobox
local tree_view_wikitext = p.renderJson({jsonschema=jsonschema, context=smw_res.context, property_definitions=smw_res.definitions, jsondata=jsondata})
tree_view_wikitext = frame:preprocess(tree_view_wikitext)
--tree_view = frame:callParserFunction( '#tree', { "", id="jsondata-tree", class="info-tree", minExpandLevel=2, quicksearch=true, tree_view_wikitext } ) -- doesn't render, so we add a div wrapper below
tree_view = frame:callParserFunction( '#tree', { tree_view_wikitext } )
--tree_view = frame:preprocess("{{#tree | " .. tree_view_wikitext .. " }}")
if (debug) then
mw.logObject("TREE")
mw.logObject(tree_view_wikitext)
mw.logObject(tree_view)
end
_details = '<div id="jsondata-tree" class="info-tree" >' .. tree_view .. '</div>'
end
if (template ~= nil) then
if (template ~= nil) then
-- render custom template
if (debug) then msg = msg .. "Parse \n\n" .. template .. " \n\nwith params " .. mw.dumpObject( jsondata ) .. "\n<br>" end
if (debug) then msg = msg .. "Parse \n\n" .. template .. " \n\nwith params " .. mw.dumpObject( jsondata ) .. "\n<br>" end
local stripped_jsondata={}
local stripped_jsondata={}
Line 311: Line 446:
if (type(v) ~= 'table') then stripped_jsondata[k] = v end --delete object values, not supported by wiki templates
if (type(v) ~= 'table') then stripped_jsondata[k] = v end --delete object values, not supported by wiki templates
end
end
stripped_jsondata["_details"] = _details
local child = frame:newChild{args=stripped_jsondata}
local child = frame:newChild{args=stripped_jsondata}
if ( template:sub(1, #"=") == "=" ) then template = "\n" .. template end -- add line break if template starts with heading (otherwise not rendered by mw parser)
wikitext = wikitext .. child:preprocess( template )
wikitext = wikitext .. child:preprocess( template )
elseif (mode == p.mode.header) then
end
local infobox_res = p.renderInfoBox({jsonschema=jsonschema, jsondata=jsondata})
if (template == nil and mode == p.mode.header and renderMode == "table") then
local ignore_properties = {[p.keys.category]=true} -- don't render type/category on every subclass
for j, subcategory in ipairs(schema_res.visited) do
if j > i then
local subjsonschema = schema_res.jsonschemas[subcategory]
for k, v in pairs(p.defaultArg(subjsonschema['properties'], {})) do
-- skip properties that are overwritten in subschemas, render them only once at the most specific position
ignore_properties[k] = true
end
end
end
-- render the infobox for the schema itself and every super_schema using always the global json-ld context (merged within walkJsonSchema())
-- context needs to be preprocessed with buildContext() since the generic json/table merge of the @context atttribute produces a list of strings (remote context) and context objects
-- context is already build in p.getSemanticProperties. schema_allOfMerged is used to provide the full schema for overridden properties
local infobox_res = p.renderInfoBox({jsonschema=super_jsonschema, schema_allOfMerged=jsonschema, context=smw_res.context, property_definitions=smw_res.definitions, jsondata=jsondata, ignore_properties=ignore_properties})
wikitext = wikitext .. frame:preprocess( infobox_res.wikitext )
wikitext = wikitext .. frame:preprocess( infobox_res.wikitext )
end
end
end
end
--local display_label = ""
local set_categories_in_wikitext = {}
--if (jsondata[p.keys.label] ~= nil) then display_label = p.splitString(jsondata[p.keys.label], '@')[1] end
p.tableMerge(set_categories_in_wikitext, json_res_store.res[p.keys.subcategory])  --classes/categories, nil for items
if (title.nsText ~= "Category") then wikitext = wikitext .. "\n" .. p.setCategories({categories=json_res_store.res[p.keys.category], sortkey=display_label}).wikitext end--items
if (title.nsText ~= "Category") then --items
wikitext = wikitext .. p.setCategories({categories=json_res_store.res[p.keys.subcategory], sortkey=display_label}).wikitext --classes/categories
p.tableMerge(set_categories_in_wikitext, json_res_store.res[p.keys.category]) -- categories from schema type
end
-- Todo: Consider moving the category and this block to p.getSemanticProperties with store=true. However, settings categories with @category is only possible for subobjects
if (smw_res ~= nil) then
if (smw_res ~= nil) then
local display_label = p.getDisplayLabel(json_res_store.res, smw_res.properties)
if title.nsText == "Property" then display_label = p.defaultArgPath(json_res_store.res, {p.keys.name}, display_label) end
if (debug) then msg = msg .. "Store page properties" end
if (debug) then msg = msg .. "Store page properties" end
smw_res.properties['Display title of'] = display_label --set special property display title
-- category handling
p.tableMerge(set_categories_in_wikitext, smw_res.properties[p.keys.category_pseudoproperty])
smw_res.properties[p.keys.category_pseudoproperty] = nil -- delete pseudo property
smw_res.properties['HasOswId'] = mw.title.getCurrentTitle().fullText  --set special property OswId to own title
-- label and display title handling
if display_label ~= nil then
smw_res.properties['Display title of'] = display_label --set special property display title
smw_res.properties['Display title of lowercase'] = display_label:lower() --store lowercase for case insensitive query
smw_res.properties['Display title of normalized'] = display_label:lower():gsub('[^%w]+','') --store with all non-alphanumeric chars removed for normalized query
end
p.setNormalizedLabel(smw_res.properties) --build normalized multilang label
mw.ext.displaytitle.set(display_label)
mw.ext.displaytitle.set(display_label)
--smw_res.properties['@category'] = jsondata[p.keys.category]
--smw_res.properties['@category'] = jsondata[p.keys.category]
Line 339: Line 507:
--wikitext = mw.dumpObject(smw_res.properties) .. wikitext
--wikitext = mw.dumpObject(smw_res.properties) .. wikitext
end
end
wikitext = wikitext .. "\n" .. p.setCategories({categories=set_categories_in_wikitext, sortkey=display_label}).wikitext
if (debug) then mw.logObject(res) end
if (debug) then mw.logObject(res) end
Line 348: Line 517:
-- test: mw.logObject(p.renderInfoBox({jsonschema=p.loadJson({title="JsonSchema:Entity"}).json, jsondata={uuid="123123"}}))
-- test: mw.logObject(p.renderInfoBox({jsonschema=p.loadJson({title="JsonSchema:Entity"}).json, jsondata={uuid="123123"}}))
function p.renderInfoBox(args)
function p.renderInfoBox(args)
local debug = p.defaultArg(args.debug, false)
local jsondata = p.defaultArg(args.jsondata, {})
local jsondata = p.defaultArg(args.jsondata, {})
local schema = p.defaultArg(args.jsonschema, nil)
local schema = p.defaultArg(args.jsonschema, nil) -- local schema from the perspective of the current category
local context = p.buildContext({jsonschema=schema}).context
local schema_allOfMerged = p.defaultArg(args.schema_allOfMerged, schema) -- global schema with allOfs merged
local ignore_properties = {[p.keys.category]=true} -- don't render type/category on every subclass
local property_definitions = p.defaultArg(args.property_definitions, {}) -- dict schema_key: {property: <smw_property>, ...}
local res = ""
local res = ""
if schema == nil then return res end
if schema == nil then return res end
local schema_label = ""
if schema['title'] ~= nil then schema_label = schema['title'] end
local context = p.defaultArg(args.context, p.buildContext({jsonschema=schema}).context)
local ignore_properties = p.defaultArg(args.ignore_properties, {})
 
local schema_label = p.renderMultilangValue({jsonschema=schema})
-- see also: https://help.fandom.com/wiki/Extension:Scribunto/HTML_Library_usage_notes
-- see also: https://help.fandom.com/wiki/Extension:Scribunto/HTML_Library_usage_notes
Line 369: Line 542:
if (not ignore_properties[k]) then
if (not ignore_properties[k]) then
if (schema['properties'] ~= nil and schema['properties'][k] ~= nil and (type(v) ~= 'table' or v[1] ~= nil)) then --literal or literal array
if (schema['properties'] ~= nil and schema['properties'][k] ~= nil and (type(v) ~= 'table' or v[1] ~= nil)) then --literal or literal array
local def = schema['properties'][k]
local def = schema_allOfMerged['properties'][k]
--mw.logObject(def)
--mw.logObject(def)
local label = k
if def['title'] ~= nil then label = def['title'] end
local label = p.renderMultilangValue({jsonschema=def, default=k})
local description = p.renderMultilangValue({jsonschema=def, key="description"})
if (p.tableLength(p.defaultArgPath(property_definitions, {k, 'defined_in'}, {})) > 0) then description = description .. "<br>Definition: " end
for i, c in pairs(p.defaultArgPath(property_definitions, {k, 'defined_in'}, {})) do
if (i > 1) then description = description .. ", " end
description = description .. "[[:" ..c .. "]]"
end
if (description ~= "") then description = "{{#info: " .. description .. "|note }}" end -- smw tooltip
label = label .. description
 
--res = res .. title ": " .. v
--res = res .. title ": " .. v
local cell = tbl:tag( 'tr' )
local cell = tbl:tag( 'tr' )
Line 383: Line 566:
if (type(e) ~= 'table') then  
if (type(e) ~= 'table') then  
local p_type = p.defaultArgPath(context, {k, '@type'}, '@value')
local p_type = p.defaultArgPath(context, {k, '@type'}, '@value')
if (p_type == '@id') then e = "[[" .. string.gsub(e, "Category:", ":Category:") .. "]]" end
if (p_type == '@id' and p.defaultArgPath(def, {'items', 'type'}, 'unknown') == 'string' and def['eval_template'] == nil) then
-- auto-link (OSW-)IDs if no eval_template is present
e = string.gsub(e, "Category:", ":Category:") -- make sure category links work
e = string.gsub(e, "File:", ":File:") -- do not embedd images but link to them
e = "[[" .. e .. "]]"
elseif (p_type == 'xsd:date') then -- formate date with user preferences
e = "{{#dateformat:" .. e .. "|ymd}}"
elseif (p_type == 'xsd:dateTime') then -- formate time with user preferences
local smw_property = p.defaultArgPath(property_definitions, {k, 'property'})
if (smw_property ~= nil) then e = "{{#ask: [[{{FULLPAGENAME}}]]|?" .. smw_property .. "#LOCL#TO= |format=plain |mainlabel=-}}"
else
local _, _, date, hours, minutes = string.find(e, "(%S+)[T ](%S+)[:](%S+)[:?]")
e = "{{#dateformat:" .. date .. "|ymd}} " .. hours .. ":" .. minutes .. " (UTC)"
end
elseif (type(v) == 'boolean') then
if (v) then v = "&#x2705;" else v = "&#x274C;" end -- green check mark or red cross
elseif (def['eval_template'] == nil and (string.len(e) > 100) and (string.find(e, "{{") == nil) and (string.find(e, "</") == nil) and (string.find(e, "%[%[") == nil)) then -- no markup, no links
e = string.sub(e, 1, 100) .. "..."; -- limit infobox plain text to max 100 chars
elseif (debug) then
mw.log("Unformated: " .. k .. " " .. p.defaultArgPath(def, {'items', 'type'}, 'unknown'))
mw.logObject(def)
end
cell:wikitext("\n* " .. e .. "")  
cell:wikitext("\n* " .. e .. "")  
end
end
Line 389: Line 593:
else
else
local p_type = p.defaultArgPath(context, {k, '@type'}, '@value')
local p_type = p.defaultArgPath(context, {k, '@type'}, '@value')
if (p_type == '@id') then v = "[[" .. string.gsub(v, "Category:", ":Category:") .. "]]" end
if (p_type == '@id' and p.defaultArgPath(def, {'type'}, 'unknown') == 'string' and def['eval_template'] == nil) then  
cell:wikitext( v )
-- auto-link (OSW-)IDs if no eval_template is present
v = string.gsub(v, "Category:", ":Category:") -- make sure category links work
v = string.gsub(v, "File:", ":File:") -- do not embedd images but link to them
v = "[[" .. v .. "]]"
elseif (p_type == 'xsd:date') then -- formate date & time with user preferences
v = "{{#dateformat:" .. v .. "|ymd}}"
elseif (p_type == 'xsd:dateTime') then -- formate time with user preferences
local smw_property = p.defaultArgPath(property_definitions, {k, 'property'})
if (smw_property ~= nil) then v = "{{#ask: [[{{FULLPAGENAME}}]]|?" .. smw_property .. "#LOCL#TO= |format=plain |mainlabel=-}}"
else
local _, _, date, hours, minutes = string.find(v, "(%S+)[T ](%S+)[:](%S+)[:?]")
v = "{{#dateformat:" .. date .. "|ymd}} " .. hours .. ":" .. minutes .. " (UTC)"
end
elseif (type(v) == 'boolean') then
if (v) then v = "&#x2705;" else v = "&#x274C;" end -- green check mark or red cross
elseif (def['eval_template'] == nil and (string.len(v) > 100) and (string.find(v, "{{") == nil) and (string.find(v, "</") == nil) and (string.find(v, "%[%[") == nil)) then -- no markup, no links
v = string.sub(v, 1, 100) .. "..."; -- limit infobox plain text to max 100 chars
elseif (debug) then
mw.log("Unformated: " .. k .. " " .. p.defaultArgPath(def, {'type'}, 'unknown'))
mw.logObject(def)
end
cell:wikitext("\n" .. v .. "")
end
end
end
end
Line 399: Line 624:
return {wikitext=res}
return {wikitext=res}
end
function p.renderLiteral(args)
local frame = mw.getCurrentFrame()
local debug = p.defaultArg(args.debug, false)
local key = p.defaultArg(args.key, nil)
local value = p.defaultArg(args.value, nil)
local schema = p.defaultArg(args.schema, nil) -- local property schema
local property_definitions = p.defaultArg(args.property_definitions, {}) -- {property: <smw_property>, ...}
    local level = p.defaultArg(args.level, 0)
    local result = ""
   
    -- Create the indentation prefix for the main level
    local prefix = string.rep("*", level + 1)
   
    -- Get the label/title from schema
    local label = p.renderMultilangValue({jsonschema=schema, default=key})
   
    -- format label bold
    label = "'''" .. label .. "'''"
   
    -- display further information in tooltip
local description = p.renderMultilangValue({jsonschema=schema, key="description"})
if (p.tableLength(p.defaultArgPath(property_definitions, {key, 'defined_in'}, {})) > 0) then description = description .. "<br>Definition: " end
for i, c in pairs(p.defaultArgPath(property_definitions, {key, 'defined_in'}, {})) do
if (i > 1) then description = description .. ", " end
description = description .. "[[:" ..c .. "]]"
end
if (description ~= "") then description = "{{#info: " .. description .. "|note }}" end -- smw tooltip
label = label .. description
--label = frame:preprocess(label) -- we need to evaluate the wikitext before we pass it to the tree tag
   
    -- Helper function to check if value is an array
    local function isArray(v)
        return type(v) == "table" and v[1] ~= nil
    end
   
    -- Helper function to convert value to string
    local function valueToString(v)
        if type(v) == "boolean" then
            return tostring(v)
        elseif type(v) == "nil" then
            return "nil"
        elseif type(v) == "string" then
        return p.wrapLinkIfNs(v)
        else
            return tostring(v)
        end
    end
   
    -- Handle different value types
    if not isArray(value) then
        -- Single literal value
        result = prefix .. " " .. label .. ": " .. valueToString(value) .. "\n"
       
    elseif #value == 1 then
        -- Array with single element
        result = prefix .. " " .. label .. ": " .. valueToString(value[1]) .. "\n"
       
    else
        -- Array with multiple elements (length > 1)
        result = prefix .. " " .. label .. ":\n"
        local nestedPrefix = string.rep("*", level + 2)
        if value ~= nil then
        for _, item in ipairs(value) do
            result = result .. nestedPrefix .. " " .. valueToString(item) .. "\n"
        end
    end
    end
   
    return result
end
function p.renderJson(args)
local frame = mw.getCurrentFrame()
local debug = p.defaultArg(args.debug, false)
local jsondata = p.defaultArg(args.jsondata, {})
local jsonschema = p.defaultArg(args.jsonschema, nil) -- global schema with allOfs merged
local property_definitions = p.defaultArg(args.property_definitions, {}) -- dict schema_key: {property: <smw_property>, ...}
local level = p.defaultArg(args.level, 0)
    local result = ""
   
    -- Helper function to check if a table is an array (has numeric indices)
    local function isArray(t)
        if type(t) ~= "table" then
            return false
        end
        return t[1] ~= nil
    end
   
    -- Helper function to check if a value is a literal (not a table)
    local function isLiteral(v)
        return type(v) ~= "table"
    end
   
    -- Try detect and render quantity objects
    local function isQuantityObject(val, def)
        if type(val) ~= 'table' then return false end
        local num = val.numerical_value or val.value or val.amount
        local unit = val.unit
        if num ~= nil and (type(unit) == 'string' or unit == nil) then
            return true
        end
        if type(def) == 'table' and type(def.properties) == 'table' then
            local hasNumDef = def.properties.numerical_value or def.properties.value or def.properties.amount
            local hasUnitDef = def.properties.unit
            if hasNumDef and hasUnitDef then return true end
        end
        return false
    end
    local function renderQuantity(val)
        local num = val and (val.numerical_value or val.value or val.amount)
        local unit = val and val.unit
        if num == nil then return nil end
        local unitTxt = unit and p.wrapLinkIfNs(unit) or ""
        if unitTxt ~= "" then return tostring(num) .. " " .. unitTxt end
        return tostring(num)
    end
   
    -- Helper function to get propertyOrder from schema
    local function getPropertyOrder(key)
        if jsonschema and jsonschema.properties and jsonschema.properties[key] then
            local propSchema = jsonschema.properties[key]
            if propSchema.propertyOrder then
                return propSchema.propertyOrder
            end
        end
        -- Default order for properties without explicit order
        return 1000000 -- High value to place them at the end
    end
   
    -- Collect all keys and sort them by propertyOrder
    local sortedKeys = {}
    for key, _ in pairs(jsondata) do
        table.insert(sortedKeys, key)
    end
   
    table.sort(sortedKeys, function(a, b)
        return getPropertyOrder(a) < getPropertyOrder(b)
    end)
   
    -- Iterate over sorted keys
    for _, key in ipairs(sortedKeys) do
        local value = jsondata[key]
        local do_render = true
        -- Look up the property definition in the schema
        local propertySchema = nil
        if jsonschema and jsonschema.properties then
            propertySchema = jsonschema.properties[key]
            if p.defaultArgPath(propertySchema, {"options", "hidden"}, false) == true then
            do_render = false
            end
        end
       
        if do_render then
       
        if isLiteral(value) then
            -- Single literal value (string, number, boolean, nil)
            result = result .. p.renderLiteral({key=key, value=value, schema=propertySchema, property_definitions=property_definitions, level=level, debug=debug})
           
        elseif isArray(value) then
            -- Array - determine if it contains literals or objects
            if #value == 0 or isLiteral(value[1]) then
                -- Empty array or array of literals
                result = result .. p.renderLiteral({key=key, value=value, schema=propertySchema, property_definitions=property_definitions, level=level, debug=debug})
            else
                -- Array of objects - recurse for each item
                local itemSchema = propertySchema and propertySchema.items
                for _, item in ipairs(value) do
if isQuantityObject(item) then
    result = result .. p.renderLiteral({key=key, value=renderQuantity(item), schema=itemSchema, property_definitions=property_definitions, level=level, debug=debug})
    else
                result = result .. p.renderLiteral({key=key, value="", schema=propertySchema, property_definitions=property_definitions, level=level, debug=debug})
                    result = result .. p.renderJson({jsondata=item, jsonschema=itemSchema, level=level + 1, debug=debug})
                    end
                end
            end
           
        else
            -- Single object - recurse with incremented level
            if isQuantityObject(value) then
result = result .. p.renderLiteral({key=key, value=renderQuantity(value), schema=propertySchema, property_definitions=property_definitions, level=level, debug=debug})
else
            result = result .. p.renderLiteral({key=key, value="", schema=propertySchema, property_definitions=property_definitions, level=level, debug=debug})
            result = result .. p.renderJson({jsondata=value, jsonschema=propertySchema, level=level + 1, debug=debug})
            end
        end
  end
    end
   
    return result
end
end


Line 407: Line 824:
local jsonschema = p.defaultArg(args.jsonschema, {})
local jsonschema = p.defaultArg(args.jsonschema, {})
local includeNamespace = p.defaultArg(args.includeNamespace, false)
local includeNamespace = p.defaultArg(args.includeNamespace, false)
local includeSchemas = p.defaultArg(args.includeSchemas, false)
local categories = {}
local categories = {}
Line 413: Line 831:
--properties['@category'] = {}
--properties['@category'] = {}
for k, entry in pairs(allOf) do
for k, entry in pairs(allOf) do
if type(entry) == 'table' then -- "allOf": [{"$ref": "/wiki/Category:Test?action=raw"}]
local refs = nil
for p, v in pairs(entry) do
if type(entry) == 'table' then refs = entry -- "allOf": [{"$ref": "/wiki/Category:Test?action=raw"}]
if (p == '$ref') then
else refs = {entry} end-- "allOf": {"$ref": "/wiki/Category:Test?action=raw"}
for category in v:gmatch("Category:([^?]+)") do -- e.g. "/wiki/Category:Test?action=raw"
for p, v in pairs(entry) do
if (includeNamespace) then category = "Category:" .. category end
if (p == '$ref') then
    table.insert(categories, category)
for category in v:gmatch("Category:([^?]+)") do -- e.g. "/wiki/Category:Test?action=raw"
if (includeNamespace) then category = "Category:" .. category end
    table.insert(categories, category)
end
if includeSchemas then
for schema in v:gmatch("JsonSchema:([^?]+)") do -- e.g. "/wiki/JsonSchema:Test?action=raw"
if (includeNamespace) then schema = "JsonSchema:" .. schema end
    table.insert(categories, schema)
end
end
end
end
end
end
else -- "allOf": {"$ref": "/wiki/Category:Test?action=raw"}
if (k == '$ref') then
for category in entry:gmatch("Category:([^?]+)") do -- e.g. "/wiki/Category:Test?action=raw"
if (includeNamespace) then category = "Category:" .. category end
table.insert(categories, category)
end
end
end
end
end
end
Line 454: Line 872:
jsonschema = p.expandJsonRef({json=p.loadJson({title=category, slot="jsonschema"}).json}).json
jsonschema = p.expandJsonRef({json=p.loadJson({title=category, slot="jsonschema"}).json}).json
mw.logObject(p.buildContext({jsonschema=jsonschema, debug=true}))
mw.logObject(p.buildContext({jsonschema=jsonschema, debug=true}))
mw.log(mw.text.jsonEncode(p.buildContext({jsonschema=jsonschema, debug=false}).context))
or
or
jsonschema = {
jsonschema = {
Line 476: Line 895:
--]]
--]]


-- constructs a property specific local jsonld context
function p.buildContext(args)
function p.buildContext(args)
local schema = p.defaultArg(args.jsonschema, {})
local schema = p.defaultArg(args.jsonschema, {})
Line 487: Line 907:
elseif (type(v) == 'table' and v[1] ~= nil) then --custom addtional mappings, e. g. "type*": ["Property:HasType"]
elseif (type(v) == 'table' and v[1] ~= nil) then --custom addtional mappings, e. g. "type*": ["Property:HasType"]
result[k] = v
result[k] = v
elseif (type(v) == 'table' and v['@id'] == nil) then --subcontext
elseif (type(v) == 'table' and v['@id'] == nil and v['@reverse'] == nil) then --subcontext
p.tableMerge(result, p.buildContext({context=v}).context)
p.tableMerge(result, p.buildContext({context=v}).context)
else  
else  
Line 496: Line 916:
local properties = p.defaultArg(schema.properties, {})
local properties = p.defaultArg(schema.properties, {})


-- build property context
for k,v in pairs(properties) do
for k,v in pairs(properties) do
local subcontext = nil
local subcontext = nil
Line 502: Line 923:
subcontext = p.buildContext({jsonschema=properties[k]}).context
subcontext = p.buildContext({jsonschema=properties[k]}).context
elseif (p.defaultArgPath(properties, {k, 'items', 'type'}) == 'object') then  
elseif (p.defaultArgPath(properties, {k, 'items', 'type'}) == 'object') then  
mw.logObject(properties[k]['items'])
--mw.logObject(properties[k]['items'])
subcontext = p.buildContext({jsonschema=properties[k]['items']}).context
subcontext = p.buildContext({jsonschema=properties[k]['items']}).context
end
end
Line 541: Line 962:
local schema = p.defaultArg(args.jsonschema, {})
local schema = p.defaultArg(args.jsonschema, {})
local subschema = p.defaultArg(args.subschema, schema)
local subschema = p.defaultArg(args.subschema, schema)
local parent_schema_property = p.defaultArg(args.parent_schema_property, {})
local parent_schema_property = p.defaultArg(args.parent_schema_property, {}) -- ToDo: Not used except in getSemanticQuery => remove
local store = p.defaultArg(args.store, false)
local store = p.defaultArg(args.store, false)
local root = p.defaultArg(args.root, true)
local root = p.defaultArg(args.root, true)
local properties = p.defaultArg(args.properties, {}) --semantic properties to store, dict key=property_name, value=array of string values
local debug = p.defaultArg(args.debug, false)
local debug = p.defaultArg(args.debug, false)
--if (debug) then mw.logObject("Call getSemanticProperties with args " .. mw.dumpObject( args ) .. "\n<br>") end
--if (debug) then mw.logObject("Call getSemanticProperties with args " .. mw.dumpObject( args ) .. "\n<br>") end
local properties = {} --semantic properties
local subjectId = mw.title.getCurrentTitle().fullText
local subobjectId = nil
if (root == false and jsondata['uuid'] ~= nil) then
subobjectId = "OSW" .. string.gsub(jsondata['uuid'], "-", "")
subjectId = subjectId .. '#' .. subobjectId
end
-- create smw quantity property within the quantity value subobject
-- properties = p.processQuantityValue({properties=properties, value_object=jsondata, schema=subschema, debug=debug}).properties
local property_data = {}
local property_data = {}
local context = p.defaultArg(args.context, p.buildContext({jsonschema=schema}).context)
local context = p.defaultArg(args.context, p.buildContext({jsonschema=schema}).context)
Line 553: Line 984:
if (debug) then mw.logObject(context) end
if (debug) then mw.logObject(context) end
if schema ~= nil and context ~= nil then
if schema ~= nil and context ~= nil then
local schema_properties = p.defaultArg(subschema.properties, {})
local schema_properties = p.defaultArg(subschema.properties, nil)
if schema_properties == nil then schema_properties = p.defaultArgPath(subschema,  {"items", "properties"},  {}) end -- array schema
if (debug and root) then
if (debug and root) then
for k,v in pairs(context) do
for k,v in pairs(context) do
Line 563: Line 995:
for k,v in pairs(jsondata) do
for k,v in pairs(jsondata) do
local property_names = {}
local property_names = {}
local subobject_properties = {} -- reverse properties to store in the subobject
local mapping_found = false
local mapping_found = false
local property_definitions = {}
local property_definitions = {} -- list of objects {id=..., reverse=...}
if (context[k] ~= nil) then --json-ld mapping
 
if type(context[k]) == 'table' then table.insert(property_definitions, context[k]["@id"])
else table.insert(property_definitions, context[k]) end
end
for term, def in pairs(context) do
for term, def in pairs(context) do
local term_parts = p.splitString(term, "*")
local term_parts = p.splitString(term, "*")
if (string.find(term, "*", 0, true) and term_parts[1] == k) then --custom additional mapping term*(*...): "Property:..."
if (term_parts[1] == k) then --custom additional mapping term*(*...): "Property:..."
if type(def) == 'table' then table.insert(property_definitions, def["@id"])
if type(def) == 'table' then
else table.insert(property_definitions, def) end
-- note: json-ld allows only @id OR @reverse
if (def["@id"] ~= nil) then table.insert(property_definitions, {id=def["@id"], reverse=false}) end
if (def["@reverse"] ~= nil) then table.insert(property_definitions, {id=def["@reverse"], reverse=true}) end
else table.insert(property_definitions, {id=def}) end
end
end
end
end
if (debug) then mw.logObject(property_definitions) end
if (debug) then mw.logObject(property_definitions) end
for i,e in ipairs(property_definitions) do  
for i,e in ipairs(property_definitions) do  
local property_definition = p.splitString(e, ':')
local id = e["id"]
local property_definition = p.splitString(id, ':')
if property_definition[1] == p.keys.property_ns_prefix then
if property_definition[1] == p.keys.property_ns_prefix then
mapping_found = true
mapping_found = true
property_name = string.gsub(e, p.keys.property_ns_prefix .. ":", "") -- also allow prefix properties like: Property:schema:url
property_name = string.gsub(id, p.keys.property_ns_prefix .. ":", "") -- also allow prefix properties like: Property:schema:url
table.insert(property_names, property_name)
if (e["reverse"]) then -- reverse properties are handled in the respective subobject
if (subobject_properties[property_name] == nil) then subobject_properties[property_name] = {} end --initialize empty list
table.insert(subobject_properties[property_name], subjectId) -- add triple subobject -property-> subject
else table.insert(property_names, property_name) end
local schema_property = p.defaultArg(schema_properties[k], {})
local schema_property = p.defaultArg(schema_properties[k], {})
local schema_type = p.defaultArg(schema_property.type, nil) --todo: also load smw property type on demand
local schema_type = p.defaultArg(schema_property.type, nil) --todo: also load smw property type on demand
property_data[k] = {schema_type=schema_type, schema_data=schema_property, property=property_name, value=v}
property_data[k] = {schema_type=schema_type, schema_data=schema_property, property=property_name, value=v, reverse=e["reverse"]}
end
end
end
end
Line 597: Line 1,034:
context = p.tableMerge(context, subcontext) -- pull up nested context
context = p.tableMerge(context, subcontext) -- pull up nested context
local values = {}
local values = {}
if (v[1] == nil) then --key value array = object/dict
if (p.tableLength(v) > 0 and v[1] == nil) then --key value array = object/dict => subobject
local subproperties_res = p.getSemanticProperties({jsonschema=schema, jsondata=v, store=true, root=false, debug=debug, context=context, subschema=schema_properties[k], parent_schema_property=property_data[k]})
local subproperties_res = p.getSemanticProperties({jsonschema=schema, jsondata=v, properties=p.copy(subobject_properties), store=true, root=false, debug=debug, context=context, subschema=schema_properties[k], parent_schema_property=property_data[k]})
local id = subproperties_res.id --subobject_id
local id = subproperties_res.id --subobject_id
if (id ~= nil) then  
if (id ~= nil) then  
Line 604: Line 1,041:
table.insert(values, id)  
table.insert(values, id)  
end
end
-- create statement shortcut on the parent object
properties = p.processStatement({subject=properties, statement=subproperties_res.properties, debug=debug}).subject
properties = p.processStatement({subject=properties, statement=subproperties_res.properties, debug=debug}).subject
else --list array
else --list array
for i, e in pairs(v) do
for i, e in pairs(v) do
if (type(e) == 'table') then  
if (type(e) == 'table') then  
local subproperties_res = p.getSemanticProperties({jsonschema=schema, jsondata=e, store=true, root=false, debug=debug, context=context, subschema=schema_properties[k], parent_schema_property=property_data[k]})
local subproperties_res = p.getSemanticProperties({jsonschema=schema, jsondata=e, properties=p.copy(subobject_properties), store=true, root=false, debug=debug, context=context, subschema=schema_properties[k], parent_schema_property=property_data[k]})
local id = subproperties_res.id --subobject_id
local id = subproperties_res.id --subobject_id
if (id ~= nil) then  
if (id ~= nil) then  
Line 614: Line 1,052:
table.insert(values, id)  
table.insert(values, id)  
end
end
-- create statement shortcut on the parent object
properties = p.processStatement({subject=properties, statement=subproperties_res.properties, debug=debug}).subject
properties = p.processStatement({subject=properties, statement=subproperties_res.properties, debug=debug}).subject
else values = v end --plain strings
else values = v end --plain strings
Line 623: Line 1,062:
end
end
else if (debug) then mw.logObject("not mapped: " .. k .. " = " .. mw.dumpObject(v)) end  
else if (debug) then mw.logObject("not mapped: " .. k .. " = " .. mw.dumpObject(v)) end  
end
-- create smw quantity property on the parent object
if (p.tableLength(v) > 0 and v[1] == nil) then --key value array = object/dict => subobject
properties = p.processQuantityValue({properties=properties, value_object=v, schema=schema_properties[k], debug=debug}).properties
else --list array
for i, e in pairs(v) do
if (type(e) == 'table') then
properties = p.processQuantityValue({properties=properties, value_object=e, schema=schema_properties[k], debug=debug}).properties
end
end
end
end
else
else
Line 637: Line 1,087:
end
end
local subobjectId = nil
local store_res = nil
local store_res = nil
if (store) then  
if (store) then  
properties['HasOswId'] = subjectId
if (root) then  
if (root) then  
if (debug) then mw.logObject("Store page properties") end
if (debug) then mw.logObject("Store page properties") end
store_res = mw.smw.set( properties ) --store as semantic properties
store_res = mw.smw.set( properties ) --store as semantic properties
else
else
properties['@category'] = {}
p.tableMerge(properties['@category'], jsondata[p.keys.category]) -- from json property 'type'
p.tableMerge(properties['@category'], properties[p.keys.category_pseudoproperty]) -- from json-ld context 'Property:Category'
properties[p.keys.category_pseudoproperty] = nil -- delete pseudo property
if jsondata['uuid'] ~= nil then subobjectId = "OSW" .. string.gsub(jsondata['uuid'], "-", "") end
local display_label = p.getDisplayLabel(jsondata, properties)
properties['@category'] = jsondata[p.keys.category]
if properties['Display title of'] == nil and properties['Display_title_of'] == nil then
if (jsondata[p.keys.name] ~= nil) then properties['Display title of'] = jsondata[p.keys.name]
if (display_label ~= nil and display_label ~= "") then properties['Display title of'] = display_label
elseif (jsondata[p.keys.label] ~= nil and jsondata[p.keys.label][1] ~= nil) then properties['Display title of'] = p.splitString(jsondata[p.keys.label][1], '@')[1]
else properties['Display title of'] = p.defaultArg(subschema['title'], "") end -- fall back to property name in schema
else properties['Display title of'] = p.defaultArg(parent_schema_property.schema_data['title'], "") end
end
p.setNormalizedLabel(properties) --build normalized multilang label
if (p.tableLength(properties) > 0) then
if (p.tableLength(properties) > 0) then
store_res = mw.smw.subobject( properties, subobjectId ) --store as subobject
store_res = mw.smw.subobject( properties, subobjectId ) --store as subobject
Line 662: Line 1,117:
if (debug) then mw.logObject(error) end
if (debug) then mw.logObject(error) end
return {properties=properties, definitions=property_data, id=subobjectId, context=context, error=error}
return {properties=properties, definitions=property_data, id=subobjectId, context=context, error=error}
end
function p.processQuantityValue(args)
local properties = p.defaultArg(args.properties, {})
local object = p.defaultArg(args.value_object) -- {value: 1.1, unit: "Item:..."}
local schema = p.defaultArg(args.schema) -- {title: "Length", properties: {unit: {default: "Item:...", enum: ["Item:...", ...], options: enum_titles: ["m", ...]}}}
local debug = p.defaultArg(args.debug, false)
if debug then
mw.log("Check for quantity value object")
mw.logObject(object)
mw.logObject(schema)
end
if (object.value ~= nil and schema[p.keys.smw_quantity_property] ~= nil and schema.properties ~= nil and schema.properties.value ~= nil
and schema.properties.unit ~= nil and schema.properties.unit.enum ~= nil and schema.properties.unit.options ~= nil and schema.properties.unit.options.enum_titles ~= nil) then
object.property = string.gsub(schema[p.keys.smw_quantity_property], p.keys.property_ns_prefix .. ":", "")
object.value = p.defaultArg(object.value, schema.properties.value.default)
object.unit = p.defaultArg(object.unit, schema.properties.unit.default)
for i, e in pairs(schema.properties.unit.enum) do
if e == object.unit then object.unit_index = i end
end
-- only map internal properties (Properties:...) and if a unit was found
if (object.unit_index ~= nil and p.splitString(schema[p.keys.smw_quantity_property], ':')[1] == p.keys.property_ns_prefix) then
if debug then
mw.log("Create quantity value")
mw.logObject(object)
end
object.unit_symbol = schema.properties.unit.options.enum_titles[object.unit_index]
if (properties[object.property] == nil) then properties[object.property] = {} end
table.insert(properties[object.property], object.value .. " " .. object.unit_symbol)
end
end
return {properties=properties}
end
end


Line 672: Line 1,161:
if (statement["HasSubject"] == nil or statement["HasSubject"][1] == nil or statement["HasSubject"][1] == "") then --implicit subject
if (statement["HasSubject"] == nil or statement["HasSubject"][1] == nil or statement["HasSubject"][1] == "") then --implicit subject
if (statement["HasProperty"] ~= nil and statement["HasProperty"][1] ~= nil and statement["HasProperty"][1] ~= "" and statement["HasObject"] ~= nil) then
if (statement["HasProperty"] ~= nil and statement["HasProperty"][1] ~= nil and statement["HasProperty"][1] ~= "" and statement["HasObject"] ~= nil) then
local property = p.splitString(statement["HasProperty"][1], ":")[2]
local property = string.gsub(statement["HasProperty"][1], p.keys.property_ns_prefix .. ":", "") -- also allow prefix properties like: Property:schema:url
if (debug) then
if (debug) then
mw.log("Set property " .. property .. " from statement to ")
mw.log("Set property " .. property .. " from statement to ")
Line 765: Line 1,254:
     if type(v) == "table" then
     if type(v) == "table" then
             json[k] = p.expandJsonRef({json=v}).json
             json[k] = p.expandJsonRef({json=v}).json
        end
    end
    local result = p.copy(json)
    for k,v in pairs(json) do
    if (k == "allOf") then
            if (type(v) == "table" and v[1] == nil) then v = {v} end -- ensure array
            for i,s in pairs(v) do
            result = p.tableMerge(s, result)
            if (debug) then mw.log("merge allOf with title " .. s["title"]) end
            end
            result[k] = nil
         end
         end
     end
     end
      
      
     return {json=json}
     return {json=result}
end
end


function p.defaultArg(arg, default)
function p.defaultArg(arg, default)
Line 848: Line 1,350:
--test: mw.logObject(p.tableMerge({"string", test1="test1", subtable1={"test"}}, {"string2", test1="test2", test3="test4"}))
--test: mw.logObject(p.tableMerge({"string", test1="test1", subtable1={"test"}}, {"string2", test1="test2", test3="test4"}))
function p.tableMerge(t1, t2)
function p.tableMerge(t1, t2)
if (t1 == nil) then t1 = {} elseif (type(t1) ~= 'table') then t1 = {t1} end
if (t2 == nil) then t2 = {} elseif (type(t2) ~= 'table') then t2 = {t2} end
     for k,v in pairs(t2) do
     for k,v in pairs(t2) do
         if type(v) == "table" then
         if type(v) == "table" then
Line 873: Line 1,377:
   for k, v in pairs(obj) do res[p.copy(k, s)] = p.copy(v, s) end
   for k, v in pairs(obj) do res[p.copy(k, s)] = p.copy(v, s) end
   return res
   return res
end
-- get normalized label
function p.getDisplayLabel(jsondata, properties)
local display_label = nil
-- check if label properties are mapped
if (properties["HasLabel"] ~= nil and properties["HasLabel"][1] ~= nil) then display_label = p.splitString(properties["HasLabel"][1], '@')[1]
elseif (properties["HasName"] ~= nil) then
if type(properties["HasName"]) == 'table' then display_label = properties["HasName"][1]
else display_label = properties["HasName"] end
-- fall back to unmapped keywords
elseif (jsondata[p.keys.label] ~= nil and jsondata[p.keys.label][1] ~= nil) then
if type(jsondata[p.keys.label][1]) ~= 'table' then display_label = p.splitString(jsondata[p.keys.label][1], '@')[1]
else display_label = jsondata[p.keys.label][1][p.keys.text] end -- no eval_template applied
elseif (jsondata[p.keys.name] ~= nil) then display_label = jsondata[p.keys.name]
end
return display_label
end
-- build normalized multilang label
function p.setNormalizedLabel(properties, use_fallbacks)
if (use_fallbacks == nil) then use_fallbacks = true end
if (properties['HasLabel'] ~= nil) then
labels = properties['HasLabel']
if(type(labels) ~= 'table') then labels = {labels} end
properties['HasNormalizedLabel'] = {}
for i, label in ipairs(labels) do
label_norm = p.splitString(label, '@')[1]:lower():gsub('[^%w]+','')
label_lang = "en"
if (p.splitString(label, '@')[2] ~= nil) then label_lang = p.splitString(label, '@')[2] end
table.insert(properties['HasNormalizedLabel'], label_norm .. "@" .. label_lang)
end
elseif (use_fallbacks and properties['HasName'] ~= nil) then -- fallback, assume English lang
labels = properties['HasName']
if(type(labels) ~= 'table') then labels = {labels} end
properties['HasNormalizedLabel'] = {}
for i, label in ipairs(labels) do
label_norm = label:lower():gsub('[^%w]+','')
table.insert(properties['HasNormalizedLabel'], label_norm .. "@en")
end
elseif (use_fallbacks and properties['Display title of'] ~= nil) then -- fallback, assume English lang
labels = properties['Display title of']
if(type(labels) ~= 'table') then labels = {labels} end
properties['HasNormalizedLabel'] = {}
for i, label in ipairs(labels) do
label_norm = label:lower():gsub('[^%w]+','')
table.insert(properties['HasNormalizedLabel'], label_norm .. "@en")
end
end
end
function p.renderMultilangValue(args)
local jsondata = p.defaultArg(args.jsondata, {})
local jsonschema = p.defaultArg(args.jsonschema, {})
local key = p.defaultArg(args.key, "title")
local result = p.defaultArg(args.default, "")
local default = p.defaultArg(args.default, nil)
-- "title*": {"de": ...}
if jsonschema[key] ~= nil then
result = jsonschema[key]
default = jsonschema[key] -- default is the English value
end
if jsonschema[key .. '*'] ~= nil then -- multilang label with switch
result = "{{#switch:{{USERLANGUAGECODE}}"
for k,v in pairs(jsonschema[key .. '*']) do
if k == "en" then default = v -- override with explicit English value
else result = result .. " |" .. k .. "=" .. v end
end
if default ~= nil then result = result .. " |#default=" ..  default end
result = result .. " }}"
end
-- "some_property": [{"lang": "de", "text": ...}]
if jsondata[key] ~= nil then -- multilang label with switch
result = "{{#switch:{{USERLANGUAGECODE}}"
for k,v in pairs(jsondata[key]) do
if v["lang"] ~= nil and v["text"] ~= nil then
if v["lang"] == "en" then default = v["text"]
else result = result .. " |" .. v["lang"] .. "=" .. v["text"] end
end
end
if default ~= nil then result = result .. " |#default=" ..  default end
result = result .. " }}"
end
return result
end
function p.wrapLinkIfNs(s)
local frame = mw.getCurrentFrame()
    if type(s) ~= 'string' then return s end
    if s:find('%[%[') then return s end -- already linked
    local ns, rest = s:match('^([%a]+):(.*)$')
    if not ns or not rest or rest == '' then return s end
    if ns == 'Category' or ns == 'Item' or ns == 'File' or ns == 'Property' then
        -- If there is an anchor/subobject in the title, use the Viewer/Link template
        return frame:expandTemplate{ title = 'Viewer/Link', args = { page = s } }
        --s = "{{Viewer/Link |page= " .. s .. "}}"
    end
    return s
end
end


return p
return p