531
edits
(Update package: OSW Core) |
(Update package: OSW Core) |
||
| Line 20: | 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 34: | Line 36: | ||
query='query' | query='query' | ||
} | } | ||
p.cache = {} | |||
--loads json from a wiki page | --loads json from a wiki page | ||
| Line 40: | 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, | 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 46: | Line 50: | ||
local json = {} | local json = {} | ||
if (slot == | 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 52: | 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 .. " | 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 70: | 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 81: | 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 88: | 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 | local super_jsonschema = nil | ||
if ( | if p.splitString(category, ':')[1] == "JsonSchema" then super_jsonschema = p.loadJson({title=category, slot=p.slots.main}).json | ||
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 99: | Line 147: | ||
--mw.logObject("Store " .. category) | --mw.logObject("Store " .. category) | ||
table.insert(visited, category) | table.insert(visited, category) | ||
jsonschemas[category] = | |||
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 107: | Line 156: | ||
end | end | ||
end | end | ||
-- Process propertyOrder based on nesting level | |||
if (root) then | if (root) then | ||
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 | ||
--merge all schemas. we need to make a copy here, otherwise jsonschemas["Category:Entity"] contains the merged schema | --merge all schemas. we need to make a copy here, otherwise jsonschemas["Category:Entity"] contains the merged schema | ||
| Line 137: | 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 160: | Line 251: | ||
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 181: | 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 193: | Line 284: | ||
--evaluate single array item string as json {"self": "<value>", ".": "<value>"} => does not work since jsondata is an object | --evaluate single array item string as json {"self": "<value>", ".": "<value>"} => does not work since jsondata is an object | ||
--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}) | --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}) | ||
| Line 216: | 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 228: | 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 280: | Line 371: | ||
jsonschema = expand_res.json | jsonschema = expand_res.json | ||
--mw.log(mw.text.jsonEncode(jsonschema)) | --mw.log(mw.text.jsonEncode(jsonschema)) | ||
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 302: | Line 389: | ||
-- store metadata where properties were defined / overridden | -- store metadata where properties were defined / overridden | ||
for i, category in ipairs(schema_res.visited) do | for i, category in ipairs(schema_res.visited) do | ||
for k, v in pairs(schema_res.jsonschemas | 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] == nil then smw_res.definitions[k] = {} end | ||
if smw_res.definitions[k]['defined_in'] == nil then smw_res.definitions[k]['defined_in'] = {} end | if smw_res.definitions[k]['defined_in'] == nil then smw_res.definitions[k]['defined_in'] = {} end | ||
| Line 328: | 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) | ||
| Line 334: | Line 425: | ||
local super_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 340: | 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) | 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 ) | ||
end | |||
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 | local ignore_properties = {[p.keys.category]=true} -- don't render type/category on every subclass | ||
for j, subcategory in ipairs(schema_res.visited) do | for j, subcategory in ipairs(schema_res.visited) do | ||
if j > i then | if j > i then | ||
local subjsonschema = schema_res.jsonschemas[subcategory] | local subjsonschema = schema_res.jsonschemas[subcategory] | ||
for k, v in pairs(subjsonschema['properties']) do | 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 | -- skip properties that are overwritten in subschemas, render them only once at the most specific position | ||
ignore_properties[k] = true | ignore_properties[k] = true | ||
| Line 362: | Line 470: | ||
end | end | ||
local set_categories_in_wikitext = {} | local set_categories_in_wikitext = {} | ||
p.tableMerge(set_categories_in_wikitext, json_res_store.res[p.keys.subcategory]) --classes/categories, nil for items | p.tableMerge(set_categories_in_wikitext, json_res_store.res[p.keys.subcategory]) --classes/categories, nil for items | ||
| Line 372: | Line 478: | ||
-- Todo: Consider moving the category and this block to p.getSemanticProperties with store=true. However, settings categories with @category is only possible for subobjects | -- 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 | ||
-- category handling | -- category handling | ||
| Line 380: | Line 489: | ||
-- label and display title handling | -- label and display title handling | ||
smw_res.properties['Display title of'] = display_label --set special property display title | 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 | p.setNormalizedLabel(smw_res.properties) --build normalized multilang label | ||
mw.ext.displaytitle.set(display_label) | mw.ext.displaytitle.set(display_label) | ||
| Line 417: | Line 528: | ||
local ignore_properties = p.defaultArg(args.ignore_properties, {}) | local ignore_properties = p.defaultArg(args.ignore_properties, {}) | ||
local schema_label = | 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 435: | Line 545: | ||
--mw.logObject(def) | --mw.logObject(def) | ||
local label = | local label = p.renderMultilangValue({jsonschema=def, default=k}) | ||
local description = | 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 | 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 | for i, c in pairs(p.defaultArgPath(property_definitions, {k, 'defined_in'}, {})) do | ||
| Line 484: | Line 582: | ||
elseif (type(v) == 'boolean') then | elseif (type(v) == 'boolean') then | ||
if (v) then v = "✅" else v = "❌" end -- green check mark or red cross | if (v) then v = "✅" else v = "❌" end -- green check mark or red cross | ||
elseif ((string.len(e) > 100) and (string.find(e, "{{") == nil) and (string.find(e, "</") == nil)) then | 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 | e = string.sub(e, 1, 100) .. "..."; -- limit infobox plain text to max 100 chars | ||
elseif (debug) then | elseif (debug) then | ||
| Line 511: | Line 609: | ||
elseif (type(v) == 'boolean') then | elseif (type(v) == 'boolean') then | ||
if (v) then v = "✅" else v = "❌" end -- green check mark or red cross | if (v) then v = "✅" else v = "❌" end -- green check mark or red cross | ||
elseif ((string.len(v) > 100) and (string.find(v, "{{") == nil) and (string.find(v, "</") == nil)) then | 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 | v = string.sub(v, 1, 100) .. "..."; -- limit infobox plain text to max 100 chars | ||
elseif (debug) then | elseif (debug) then | ||
| Line 526: | 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 534: | 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 540: | 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 | ||
if type(entry) == 'table' then refs = entry -- "allOf": [{"$ref": "/wiki/Category:Test?action=raw"}] | |||
else refs = {entry} end-- "allOf": {"$ref": "/wiki/Category:Test?action=raw"} | |||
for p, v in pairs(entry) do | |||
if (p == '$ref') then | |||
table.insert(categories, | 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 | ||
end | end | ||
end | end | ||
| Line 684: | Line 975: | ||
subjectId = subjectId .. '#' .. subobjectId | subjectId = subjectId .. '#' .. subobjectId | ||
end | 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 = {} | ||
| Line 690: | 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 739: | 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, properties=p.copy(subobject_properties), 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 | ||
| Line 746: | 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 | ||
| Line 756: | 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 765: | 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 790: | Line 1,098: | ||
p.tableMerge(properties['@category'], properties[p.keys.category_pseudoproperty]) -- from json-ld context 'Property:Category' | p.tableMerge(properties['@category'], properties[p.keys.category_pseudoproperty]) -- from json-ld context 'Property:Category' | ||
properties[p.keys.category_pseudoproperty] = nil -- delete pseudo property | properties[p.keys.category_pseudoproperty] = nil -- delete pseudo property | ||
local display_label = p.getDisplayLabel(jsondata, properties) | |||
if properties['Display title of'] == nil and properties['Display_title_of'] == nil then | |||
if (display_label ~= nil and display_label ~= "") then properties['Display title of'] = display_label | |||
else properties['Display title of'] = p.defaultArg(subschema['title'], "") end -- fall back to property name in schema | |||
end | |||
p.setNormalizedLabel(properties) --build normalized multilang label | p.setNormalizedLabel(properties) --build normalized multilang label | ||
if (p.tableLength(properties) > 0) then | if (p.tableLength(properties) > 0) then | ||
| Line 806: | 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 1,032: | 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 | end | ||
| Line 1,065: | Line 1,428: | ||
end | end | ||
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 | ||
edits