FHIRFLARE-IG-Toolkit/templates/cp_view_processed_ig.html
2025-04-25 23:16:47 +10:00

1364 lines
82 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE Ig Toolkit" width="192" height="192">
<h1 class="display-5 fw-bold text-body-emphasis">{{ title }}</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
View details of the processed FHIR Implementation Guide.
</p>
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
</div>
----------------------------------------------------------------remove the buttons----------------------------------------------------------------------------->
</div>
</div>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2><i class="bi bi-info-circle me-2"></i>{{ title }}</h2>
<a href="{{ url_for('view_igs') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Package List</a>
</div>
{% if processed_ig %}
<div class="card">
<div class="card-header">Package Details</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">Package Name</dt>
<dd class="col-sm-9"><code>{{ processed_ig.package_name }}</code></dd>
<dt class="col-sm-3">Package Version</dt>
<dd class="col-sm-9">{{ processed_ig.version }}</dd>
<dt class="col-sm-3">Processed At</dt>
<dd class="col-sm-9">{{ processed_ig.processed_date.strftime('%Y-%m-%d %H:%M:%S UTC') }}</dd>
</dl>
</div>
</div>
{% if config.DISPLAY_PROFILE_RELATIONSHIPS %}
<div class="card mt-4">
<div class="card-header">Profile Relationships</div>
<div class="card-body">
<h6>Complies With</h6>
{% if complies_with_profiles %}
<ul>
{% for profile in complies_with_profiles %}
<li>{{ profile }}</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted"><em>No profiles declared as compatible.</em></p>
{% endif %}
<h6>Required Dependent Profiles (Must Also Validate Against)</h6>
{% if imposed_profiles %}
<ul>
{% for profile in imposed_profiles %}
<li>{{ profile }}</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted"><em>No imposed profiles.</em></p>
{% endif %}
</div>
</div>
{% endif %}
<div class="card mt-4">
<div class="card-header">Resource Types Found / Defined</div>
<div class="card-body">
{% if profile_list or base_list %}
<p class="mb-2">
<small>
<span class="badge bg-warning text-dark border me-1" style="font-size: 0.8em;" title="Profile contains Must Support elements">MS</span> = Contains Must Support Elements<br>
<span class="badge bg-info text-dark border me-1" style="font-size: 0.8em;" title="An optional Extension which contains Must Support sub-elements (e.g., value[x], type)">Optional MS Ext</span> = Optional Extension with Must Support Sub-Elements
</small>
</p>
{% if profile_list %}
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = Examples will be displayed when selecting profile Types if contained in the IG</small></p>
<h6>Profiles Defined ({{ profile_list|length }}):</h6>
<div class="d-flex flex-wrap gap-1 mb-3 resource-type-list">
{% for type_info in profile_list %}
<a href="#" class="resource-type-link text-decoration-none"
data-package-name="{{ processed_ig.package_name }}"
data-package-version="{{ processed_ig.version }}"
data-resource-type="{{ type_info.name }}"
aria-label="View structure for {{ type_info.name }}">
{% if type_info.must_support %}
{% if optional_usage_elements.get(type_info.name) %}
<span class="badge bg-info text-dark border" title="Optional Extension with Must Support sub-elements">{{ type_info.name }}</span>
{% else %}
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
{% endif %}
{% else %}
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
{% endif %}
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted"><em>No profiles defined.</em></p>
{% endif %}
{% if base_list %}
<h6>Base Resource Types Referenced ({{ base_list|length }}):</h6>
<div class="d-flex flex-wrap gap-1 resource-type-list">
{% for type_info in base_list %}
<a href="#" class="resource-type-link text-decoration-none"
data-package-name="{{ processed_ig.package_name }}"
data-package-version="{{ processed_ig.version }}"
data-resource-type="{{ type_info.name }}"
aria-label="View structure for {{ type_info.name }}">
{% if type_info.must_support %}
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
{% else %}
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
{% endif %}
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted"><em>No base resource types referenced.</em></p>
{% endif %}
{% else %}
<p class="text-muted"><em>No resource type information extracted or stored.</em></p>
{% endif %}
</div>
</div>
<div id="structure-display-wrapper" class="mt-4" style="display: none;">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Structure Definition for: <code id="structure-title"></code></span>
<button type="button" class="btn-close" aria-label="Close" onclick="document.getElementById('structure-display-wrapper').style.display='none'; document.getElementById('example-display-wrapper').style.display='none'; document.getElementById('raw-structure-wrapper').style.display='none';"></button>
</div>
<div class="card-body">
<div id="structure-loading" class="text-center py-3" style="display: none;">
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
</div>
<div id="structure-fallback-message" class="alert alert-info" style="display: none;"></div>
<ul class="nav nav-tabs" id="structureTabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="differential-tab" data-bs-toggle="tab" href="#differential" role="tab" aria-controls="differential" aria-selected="true">Differential</a>
</li>
<li class="nav-item">
<a class="nav-link" id="snapshot-tab" data-bs-toggle="tab" href="#snapshot" role="tab" aria-controls="snapshot" aria-selected="false">Snapshot</a>
</li>
<li class="nav-item">
<a class="nav-link" id="must-support-tab" data-bs-toggle="tab" href="#must-support" role="tab" aria-controls="must-support" aria-selected="false">Must Support</a>
</li>
<li class="nav-item">
<a class="nav-link" id="key-elements-tab" data-bs-toggle="tab" href="#key-elements" role="tab" aria-controls="key-elements" aria-selected="false">Key Elements</a>
</li>
<li class="nav-item">
<a class="nav-link" id="constraints-tab" data-bs-toggle="tab" href="#constraints-tab-pane" role="tab">Constraints</a>
</li>
<li class="nav-item">
<a class="nav-link" id="terminology-tab" data-bs-toggle="tab" href="#terminology-tab-pane" role="tab">Terminology</a>
</li>
<li class="nav-item">
<a class="nav-link" id="searchparams-tab" data-bs-toggle="tab" href="#searchparams-tab-pane" role="tab">Search Params</a>
</li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="differential" role="tabpanel" aria-labelledby="differential-tab">
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
<div class="col-lg-4 col-md-3">Path</div>
<div class="col-lg-1 col-md-1 text-center">Card.</div>
<div class="col-lg-3 col-md-3">Type(s)</div>
<div class="col-lg-1 col-md-1 text-center">Status</div>
<div class="col-lg-3 col-md-4">Description</div>
</div>
<ul id="structure-tree-differential" class="list-group list-group-flush structure-tree-root"></ul>
</div>
<div class="tab-pane fade" id="snapshot" role="tabpanel" aria-labelledby="snapshot-tab">
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
<div class="col-lg-4 col-md-3">Path</div>
<div class="col-lg-1 col-md-1 text-center">Card.</div>
<div class="col-lg-3 col-md-3">Type(s)</div>
<div class="col-lg-1 col-md-1 text-center">Status</div>
<div class="col-lg-3 col-md-4">Description</div>
</div>
<ul id="structure-tree-snapshot" class="list-group list-group-flush structure-tree-root"></ul>
</div>
<div class="tab-pane fade" id="must-support" role="tabpanel" aria-labelledby="must-support-tab">
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
<div class="col-lg-4 col-md-3">Path</div>
<div class="col-lg-1 col-md-1 text-center">Card.</div>
<div class="col-lg-3 col-md-3">Type(s)</div>
<div class="col-lg-1 col-md-1 text-center">Status</div>
<div class="col-lg-3 col-md-4">Description</div>
</div>
<ul id="structure-tree-must-support" class="list-group list-group-flush structure-tree-root"></ul>
</div>
<div class="tab-pane fade" id="key-elements" role="tabpanel" aria-labelledby="key-elements-tab">
<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex">
<div class="col-lg-4 col-md-3">Path</div>
<div class="col-lg-1 col-md-1 text-center">Card.</div>
<div class="col-lg-3 col-md-3">Type(s)</div>
<div class="col-lg-1 col-md-1 text-center">Status</div>
<div class="col-lg-3 col-md-4">Description</div>
</div>
<ul id="structure-tree-key-elements" class="list-group list-group-flush structure-tree-root"></ul>
</div>
<div class="tab-pane fade" id="constraints-tab-pane" role="tabpanel" aria-labelledby="constraints-tab">
<div class="mt-3 table-responsive">
<p>Constraints defined in this profile:</p>
<table class="table table-sm table-bordered table-striped" id="constraints-table">
<thead class="table-light">
<tr>
<th>Key</th>
<th>Severity</th>
<th>Path</th>
<th>Description</th>
<th>Expression (FHIRPath)</th>
</tr>
</thead>
<tbody>
<tr><td colspan="5">No constraints found or data not loaded.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="terminology-tab-pane" role="tabpanel" aria-labelledby="terminology-tab">
<div class="mt-3 table-responsive">
<p>Terminology bindings defined in this profile:</p>
<table class="table table-sm table-bordered table-striped" id="bindings-table">
<thead class="table-light">
<tr>
<th>Path</th>
<th>Strength</th>
<th>ValueSet</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr><td colspan="4">No bindings found or data not loaded.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="searchparams-tab-pane" role="tabpanel" aria-labelledby="searchparams-tab">
<div class="mt-3">
<p>Search parameters associated with this profile:</p>
<div class="alert alert-warning" role="alert">
<strong>Note:</strong> SearchParameter resources. This functionality is NEWLY implemented, please report any bugs)
</div>
<table class="table table-sm table-bordered table-striped" id="searchparams-table">
<thead class="table-light">
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Conformance</th>
<th>Requirements / Description</th>
</tr>
</thead>
<tbody>
<tr><td colspan="4">Search Parameter data not available. Requires backend implementation.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="raw-structure-wrapper" class="mt-4" style="display: none;">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Raw Structure Definition for <code id="raw-structure-title"></code></span>
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-raw-def-button"
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy Raw Definition JSON">
<i class="bi bi-clipboard" aria-hidden="true"></i>
<span class="visually-hidden">Copy Raw Definition JSON to clipboard</span>
</button>
</div>
<div class="card-body">
<div id="raw-structure-loading" class="text-center py-3" style="display: none;">
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
</div>
<pre><code id="raw-structure-content" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto;"></code></pre>
</div>
</div>
</div>
<div id="example-display-wrapper" class="mt-4" style="display: none;">
<div class="card">
<div class="card-header">Examples for <code id="example-resource-type-title"></code></div>
<div class="card-body">
<div class="mb-3" id="example-selector-wrapper" style="display: none;">
<label for="example-select" class="form-label">Select Example:</label>
<select class="form-select form-select-sm" id="example-select"><option selected value="">-- Select --</option></select>
</div>
<div id="example-loading" class="text-center py-3" style="display: none;">
<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>
</div>
<div id="example-content-wrapper" style="display: none;">
<h6 id="example-filename" class="mt-2 small text-muted"></h6>
<div class="row">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6>Raw Content</h6>
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-raw-ex-button"
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy Raw Content">
<i class="bi bi-clipboard" aria-hidden="true"></i>
<span class="visually-hidden">Copy Raw Content to clipboard</span>
</button>
</div>
<pre><code id="example-content-raw" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto; white-space: pre;"></code></pre>
</div>
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6>Pretty-Printed JSON</h6>
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy" id="copy-pretty-json-button"
data-bs-toggle="tooltip" data-bs-placement="top" title="Copy JSON">
<i class="bi bi-clipboard" aria-hidden="true"></i>
<span class="visually-hidden">Copy JSON example to clipboard</span>
</button>
</div>
<pre><code id="example-content-json" class="p-2 d-block border bg-light language-json" style="max-height: 400px; overflow-y: auto;"></code></pre>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
{% endif %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
<script>
// --- START OF COMPLETE SCRIPT BLOCK ---
// --- Global Variables (Accessed by DOMContentLoaded) ---
let structureDisplayWrapper, structureTitle, structureLoading, structureFallbackMessage;
let structureTreeDifferential, structureTreeSnapshot, structureTreeMustSupport, structureTreeKeyElements;
let exampleDisplayWrapper, exampleSelectorWrapper, exampleSelect, exampleResourceTypeTitle;
let exampleLoading, exampleContentWrapper, exampleFilename, exampleContentRaw, exampleContentJson;
let rawStructureWrapper, rawStructureTitle, rawStructureContent, rawStructureLoading;
let copyRawDefButton, copyRawExButton, copyPrettyJsonButton;
let copyRawDefTooltipInstance = null;
let copyRawExTooltipInstance = null;
let copyPrettyJsonTooltipInstance = null;
let structureDataCache = {}; // Cache for fetched structure data
// --- Global Helper Functions ---
function parseFhirPath(path) {
if (!path || typeof path !== 'string') return [];
// Split by dot, but look ahead for '[' to handle paths like Observation.component[slice].code
const parts = path.split(/\.(?![^\[]*\])|(?=\[)/) // Split on dots NOT inside brackets, or before opening bracket
.map(part => part.replace(/\[x\]/g, '')); // Remove [x]
return parts.filter(part => part); // Remove empty strings
}
function escapeHtmlAttr(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Helper to get consistent node key
function getNodeKey(element) {
if (!element) return null;
// Use id if it looks like a structured path (contains '.'), otherwise default to path
const baseKey = (element.id && element.id.includes('.')) ? element.id : element.path;
if (!baseKey) {
console.warn("Cannot generate key: element missing structured id and path", element);
// Provide a fallback key if absolutely necessary, though this indicates data issues
return `no-key-${Math.random()}`;
}
// Append sliceName if present to ensure uniqueness for slices
return element.sliceName ? `${baseKey}::slice::${element.sliceName}` : baseKey;
}
// Helper function to initialize a node
function createNode(path, name, parentPathKey, element = null) {
// Determine flags based on element data passed from backend
const isInDifferential = element?.isInDifferential || false;
const mustSupport = element?.mustSupport || false;
return {
path: path,
name: name,
parentPathKey: parentPathKey, // Store the KEY of the parent for easier lookup
element: element,
children: [],
isInDifferential: isInDifferential, // Store flag for direct access
mustSupport: mustSupport // Store flag for direct access
// containsMustSupportSlice will be added later if applicable
};
}
// Refined Filter for Key Elements view
function filterKeyElements(node) {
if (!node || !node.element) return false; // Node must exist and have element data
const el = node.element;
// Criteria from definition:
// - Marked as mustSupport
if (el.mustSupport) return true;
// - Listed in differential (flag set by backend)
if (node.isInDifferential) return true; // Check the flag on the *node*
// - Mandatory (min cardinality > 0)
if (el.min !== undefined && el.min > 0) return true;
// - Max cardinality constrained (max defined and not '*')
// (We include any constraint, e.g., max=1 or max=0)
if (el.max !== undefined && el.max !== '*') return true;
// - Implicated in a constraint (element has constraints)
if (el.constraint && Array.isArray(el.constraint) && el.constraint.length > 0) return true;
// - Introduces slicing
if (el.slicing) return true;
// - Is a slice itself
if (el.sliceName) return true;
// - Is a modifier element
if (el.isModifier) return true;
return false; // Default to false if no criteria met
}
// --- Helper Function to Propagate MS Flag to Slicing Entries V2 ---
function flagSlicingEntriesWithMSSlices(node) {
if (!node) return false; // Base case for recursion
let containedMSSliceInChildren = false; // Flag for MS anywhere in the descendant tree
let directChildIsMSSlice = false; // Flag for MS in a direct child slice
if (node.children && node.children.length > 0) {
// Check direct children first for being MS slices
directChildIsMSSlice = node.children.some(child => child.element?.sliceName && child.mustSupport);
// Recursively check descendants AND collect whether any descendant branch contains an MS slice
node.children.forEach(child => {
// If the recursive call on ANY child returns true, it means an MS slice exists somewhere below that child
if (flagSlicingEntriesWithMSSlices(child)) {
containedMSSliceInChildren = true;
}
});
}
// Determine if this node ITSELF is a must-support slice
const isMustSupportSlice = node.element?.sliceName && node.mustSupport;
// Flag THIS node if it defines slicing AND (a direct child is an MS slice OR a deeper descendant is an MS slice)
if (node.element?.slicing && (directChildIsMSSlice || containedMSSliceInChildren)) {
node.containsMustSupportSlice = true;
// --- DEBUG LOG ---
// console.log(`>>> Flagged Slicing Entry ${node.path} as containsMustSupportSlice = true (DirectChild: ${directChildIsMSSlice}, Descendant: ${containedMSSliceInChildren})`);
// --- END DEBUG LOG ---
}
// Return true if THIS node is an MS slice OR if it was flagged as containing one below it
// This ensures the status propagates up correctly.
return isMustSupportSlice || node.containsMustSupportSlice === true;
} // End flagSlicingEntriesWithMSSlices V2
// --- Build Tree Data - V13 (Correct Parent Finding for Nested Slices) ---
function buildTreeData(elements, view, mustSupportPaths) {
const treeData = [];
const nodeMap = new Map(); // Map uses getNodeKey(element) as key
if (!elements || elements.length === 0) {
console.warn(`No elements provided for view '${view}'`);
return treeData;
}
// --- PRE-SORT elements (Keep V12 sorting) ---
try {
elements.sort((a, b) => {
const pathA = a.path || '';
const pathB = b.path || '';
const depthA = pathA.split('.').length;
const depthB = pathB.split('.').length;
if (depthA !== depthB) return depthA - depthB;
if (pathA !== pathB) return pathA.localeCompare(pathB);
const sliceA = a.sliceName || '';
const sliceB = b.sliceName || '';
return sliceA.localeCompare(sliceB);
});
} catch (e) {
console.error("Error sorting elements:", e, elements);
}
// --- END PRE-SORT ---
// --- Create Root Node (Keep V12 logic) ---
const rootElementData = elements.find(el => el.path && !el.path.includes('.'));
let rootNode;
if (rootElementData) {
const rootNodeKey = getNodeKey(rootElementData);
if (rootNodeKey) {
rootNode = createNode(rootElementData.path, rootElementData.path, 'Root', rootElementData);
treeData.push(rootNode);
nodeMap.set(rootNodeKey, rootNode);
} else {
console.error("Could not generate key for root element:", rootElementData);
return treeData;
}
} else {
console.error(`Cannot find root element in provided elements for view '${view}'. Cannot build tree.`);
return treeData;
}
const mustSupportPathsSet = mustSupportPaths instanceof Set ? mustSupportPaths : new Set(mustSupportPaths || []);
// --- Build Tree Structure ---
elements.forEach((element, index) => {
const path = element.path;
const id = element.id;
const nodeKey = getNodeKey(element);
const isSlice = !!element.sliceName;
// Optional Debug Log
const debugPaths = ['Observation.component', 'Observation.component.code', 'Observation.component.code.coding'];
const shouldDebug = debugPaths.some(p => path?.startsWith(p));
// if (shouldDebug) { // Keep this commented out unless actively debugging parent finding
// console.log(`[Debug] Processing element ${index}: Path='${path}', SliceName='${element.sliceName}', ID='${id}', Key='${nodeKey}'`);
// }
if (!path || !nodeKey) { console.warn(`Skipping element ${index} with missing path/key`, element); return; }
// Root element handling (Keep V12 logic)
if (nodeKey === getNodeKey(rootElementData)) {
rootNode.isInDifferential = element.isInDifferential || rootNode.isInDifferential;
rootNode.mustSupport = element.mustSupport || rootNode.mustSupport || mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id)) || mustSupportPathsSet.has(nodeKey);
if (element.isInDifferential && !rootNode.element.isInDifferential) { rootNode.element = element; }
return;
}
// Set flags (Keep V12 logic)
element.isInDifferential = element.isInDifferential || false;
element.mustSupport = element.mustSupport || mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id)) || mustSupportPathsSet.has(nodeKey);
// --- Parent Finding Logic (V13) ---
let parentNodeKey = getNodeKey(rootElementData); // Default to root
let parentElement = rootElementData;
let parentFound = false;
// Check 1: Direct Slice Definition (e.g., component:DiastolicBP or coding:DBPCode)
if (isSlice) {
let expectedParentId = null;
let expectedParentPath = null;
// Derive the expected parent ID/Path from the slice's ID or Path
if (id && id.includes('.')) {
// Try deriving from ID first (more specific)
// Example: id = Observation.component:DiastolicBP.code.coding:DBPCode
// We want the parent ID = Observation.component:DiastolicBP.code.coding
const idParts = id.split(':');
if (idParts.length > 1) {
const parentIdGuess = idParts.slice(0, -1).join(':');
// Check if the parent ID ends with a path segment (contains '.')
if (parentIdGuess.includes('.')) {
expectedParentId = parentIdGuess;
}
}
// Fallback: if ID didn't work well, try deriving from path
if (!expectedParentId && path && path.includes('.')) {
expectedParentPath = path; // The base path is the parent slicing entry path
}
} else if (path && path.includes('.')) {
// If no complex ID, use the path
expectedParentPath = path;
}
// Now, find the slicing entry element using the derived parent identifiers
let slicingEntryElement = null;
if (expectedParentId) {
// Prefer ID match
slicingEntryElement = elements.find(el => el.id === expectedParentId && el.slicing);
}
if (!slicingEntryElement && expectedParentPath) {
// Fallback to path match if ID didn't work or wasn't derived
slicingEntryElement = elements.find(el => el.path === expectedParentPath && !el.sliceName && el.slicing);
}
if (slicingEntryElement) {
parentElement = slicingEntryElement;
parentNodeKey = getNodeKey(parentElement);
parentFound = true;
// console.log(`[Debug] Parent Check 1 (Slice V13): ${nodeKey} -> ${parentNodeKey} (ParentID: ${expectedParentId}, ParentPath: ${expectedParentPath})`);
} else {
console.warn(`Slicing entry element NOT FOUND for expected parent (ID: ${expectedParentId}, Path: ${expectedParentPath}) needed by slice '${nodeKey}'. Assigning root.`);
parentElement = rootElementData;
parentNodeKey = getNodeKey(parentElement);
parentFound = true; // Consider parent determined (fallback)
}
}
// Check 2: Descendant of a Slice (using ID structure) (Keep V12 logic - seems okay from logs)
else if (id && id.includes(':') && id.includes('.')) {
const idParts = id.split('.');
let potentialSliceParentId = null;
for (let i = idParts.length - 2; i >= 0; i--) {
const tempId = idParts.slice(0, i + 1).join('.');
if (tempId.includes(':')) { potentialSliceParentId = tempId; break; }
}
if (potentialSliceParentId) {
const potentialParentKey = potentialSliceParentId;
if (nodeMap.has(potentialParentKey)) {
const potentialParentNode = nodeMap.get(potentialParentKey);
if (potentialParentNode?.element) {
parentNodeKey = potentialParentKey; parentElement = potentialParentNode.element; parentFound = true;
// console.log(`[Debug] Parent Check 2 (Slice Descendant ID V13): ${nodeKey} -> ${parentNodeKey} (via Map Key ${potentialParentKey})`);
} else { console.warn(`Node found in map for key ${potentialParentKey} but its element was missing. Falling back.`); }
} else {
const sliceParentElement = elements.find(el => el.id === potentialSliceParentId);
if (sliceParentElement) {
parentNodeKey = getNodeKey(sliceParentElement); parentElement = sliceParentElement; parentFound = true;
// console.log(`[Debug] Parent Check 2 (Slice Descendant ID V13): ${nodeKey} -> ${parentNodeKey} (via Element ID ${potentialSliceParentId})`);
} else { console.warn(`Slice parent element ID '${potentialSliceParentId}' not found for descendant '${id}'. Falling back.`); }
}
}
}
// Check 3 & 4: Regular or Choice Descendant (Keep V12 logic)
if (!parentFound) {
const pathParts = parseFhirPath(path);
if (pathParts.length > 1) {
const parentPathString = pathParts.slice(0, -1).join('.');
let potentialParentNode = nodeMap.get(parentPathString);
if (!potentialParentNode && path.includes('[x]')) {
const originalPathParts = path.split('.');
const parentPathWithChoice = originalPathParts.slice(0, -1).join('.');
potentialParentNode = nodeMap.get(parentPathWithChoice);
}
if (potentialParentNode?.element) {
parentElement = potentialParentNode.element;
parentNodeKey = getNodeKey(parentElement);
parentFound = true;
// console.log(`[Debug] Parent Check 3/4 (Path V13): ${nodeKey} -> ${parentNodeKey}`);
} else {
parentElement = rootElementData; parentNodeKey = getNodeKey(parentElement);
parentFound = true;
// console.warn(`[Debug] Parent Check 3/4 (Path V13): Parent node NOT FOUND for path '${parentPathString}' needed by '${nodeKey}'. Assigning root.`);
}
} else {
parentElement = rootElementData; parentNodeKey = getNodeKey(parentElement);
parentFound = true;
// console.log(`[Debug] Parent Check 3/4 (Path V13): Parent is root for ${nodeKey}`);
}
}
// --- End Parent Finding Logic ---
// --- Node Creation / Update / Attachment (Keep V12 logic) ---
let parentNode = nodeMap.get(parentNodeKey);
if (!parentNode) {
console.error(`Parent node OBJECT NOT FOUND in map for resolved key: '${parentNodeKey}' (child: ${nodeKey}). Assigning child to root.`);
parentNode = rootNode;
parentNodeKey = getNodeKey(rootElementData);
}
if (nodeMap.has(nodeKey)) {
const existingNode = nodeMap.get(nodeKey);
existingNode.element = element;
existingNode.isInDifferential = existingNode.isInDifferential || element.isInDifferential;
existingNode.mustSupport = existingNode.mustSupport || element.mustSupport;
if (existingNode.parentPathKey && existingNode.parentPathKey !== parentNodeKey) {
console.warn(`[!] Ambiguous Parent: Node ${nodeKey} already exists with parent ${existingNode.parentPathKey}, but current logic suggests parent ${parentNodeKey}. Keeping original parent.`);
} else {
existingNode.parentPathKey = parentNodeKey;
}
} else {
const pathPartsForName = parseFhirPath(path);
const baseName = pathPartsForName.length > 0 ? pathPartsForName[pathPartsForName.length - 1] : path;
const name = element.sliceName ? `${baseName}:${element.sliceName}` : baseName;
const newNode = createNode(path, name, parentNodeKey, element);
nodeMap.set(nodeKey, newNode);
if (parentNode) {
if (!parentNode.children) parentNode.children = [];
parentNode.children.push(newNode);
// console.log(`[Debug] Attaching new node ${nodeKey} to parent ${parentNodeKey}`);
} else {
console.error(`Cannot add child ${nodeKey} as parent node object is unexpectedly null.`);
}
}
}); // End elements.forEach
// --- Filtering Logic (Keep V12 logic) ---
if ((view === 'must-support' || view === 'key-elements' || view === 'differential') && treeData.length > 0 && treeData[0]) {
const filterNodesRecursively = (nodes, filterFnParam) => {
if (!nodes) return [];
return nodes.map(node => {
if (!node) return null;
const filteredChildren = filterNodesRecursively(node.children || [], filterFnParam);
const filterMatch = filterFnParam(node);
if (filterMatch || filteredChildren.length > 0) {
node.children = filteredChildren;
return node;
}
return null;
}).filter(node => node !== null);
};
let filterFunction;
if (view === 'must-support') { filterFunction = (node) => node.mustSupport || node.containsMustSupportSlice; }
else if (view === 'key-elements') { filterFunction = filterKeyElements; }
else { filterFunction = (node) => node.isInDifferential; } // Differential view
if (treeData[0]) {
treeData[0].children = filterNodesRecursively(treeData[0].children || [], filterFunction);
if (!filterFunction(treeData[0]) && (!treeData[0].children || treeData[0].children.length === 0)) {
if (view !== 'snapshot') {
console.log(`Root node ${treeData[0].path} does not meet criteria for view '${view}' and has no matching children. Clearing tree.`);
return [];
}
}
}
} // End Filtering Logic
// --- Post-processing step for MS highlighting (Keep V12 logic) ---
if (treeData.length > 0 && treeData[0]) { flagSlicingEntriesWithMSSlices(treeData[0]); }
return treeData;
} // End buildTreeData V13
// --- Render Node as List Item - V19 (Fixes isSlice error, keeps V18 logic) ---
function renderNodeAsLi(node, mustSupportPathsSet, resourceType, level = 0, view = 'snapshot') {
// --- Constants for Primitive Types ---
// REMOVED PRIMITIVE_TYPES check to align with reference rendering
if (!node || !node.path || !node.name) { console.warn(`Skipping render for invalid node:`, node); return ''; }
const el = node.element || {};
const path = el.path || node.path || `node-${level}-${Math.random()}`;
const elementId = el.id || null; // Renamed variable
const nodeKey = getNodeKey(el);
const sliceName = el.sliceName || null;
// Define isSlice based on sliceName
const isSlice = sliceName ? true : false;
const min = el.min !== undefined ? el.min : '';
const max = el.max !== undefined ? el.max : '*';
const short = el.short || '';
const definition = el.definition || short || '';
const isChoiceElement = (el.path && el.path.endsWith('[x]'));
const definesSlicing = el.slicing ? true : false;
// Must Support Logic & Icon (V15 logic)
let isMustSupport = node.mustSupport || false;
mustSupportPathsSet = mustSupportPathsSet instanceof Set ? mustSupportPathsSet : new Set(mustSupportPathsSet || []);
let mustSupportTitle = isMustSupport ? 'Must Support' : '';
let fhirPathExpression = path;
if (sliceName && isMustSupport) {
mustSupportTitle = `Must Support (Slice: ${sliceName})`;
fhirPathExpression = `${path}[sliceName='${sliceName}']`;
}
// Optional Debug Log for highlighting
// if (node.path === 'Patient.extension' || node.path === 'Patient.identifier') { console.log(...) }
const msIconClass = (isMustSupport || node.containsMustSupportSlice) ? 'text-warning' : '';
const mustSupportDisplay = (isMustSupport || node.containsMustSupportSlice) ? `<i class="bi bi-check-circle-fill ${msIconClass} ms-1" title="${escapeHtmlAttr(mustSupportTitle)}" data-bs-toggle="tooltip" data-fhirpath="${escapeHtmlAttr(fhirPathExpression)}"></i>` : '';
// Styling Logic (V15 logic)
let liClass = (isMustSupport || node.containsMustSupportSlice) ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
if (view === 'differential') {
if (!node.isInDifferential) { liClass += ' text-muted'; }
if (max === '0') { liClass += ' text-decoration-line-through'; }
}
const hasActualChildren = node.children && node.children.length > 0;
// Show toggle logic (V18 logic - reverted primitive filtering)
const showToggle = hasActualChildren || definesSlicing || (isChoiceElement && !definesSlicing);
// Collapse ID Generation (V13 logic)
let uniquePartBase = nodeKey || path;
if (!uniquePartBase || typeof uniquePartBase !== 'string') { uniquePartBase = `fallback-${level}-${Math.random()}`; }
const uniquePart = uniquePartBase.replace(/[^a-zA-Z0-9\-:_]/g, '-') + `-${view}-${level}`;
const collapseId = `collapse-${uniquePart}`;
const padding = level * 20;
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
// Type String Logic
let typeString = '<span class="text-muted">N/A</span>';
if (el.type && Array.isArray(el.type) && el.type.length > 0) {
typeString = el.type.map(t => {
let s = escapeHtmlAttr(t.code || 'Unknown');
let profiles = t.targetProfile || t.profile || [];
if (profiles.length > 0) {
const targetTypes = profiles.map(p => (p || '').split('/').pop()).filter(Boolean).join(', ');
if (targetTypes) { s += `(<span class="fst-italic" title="Profile(s): ${escapeHtmlAttr(profiles.join(', '))}">${escapeHtmlAttr(targetTypes)}</span>)`; }
}
return s;
}).join(' | ');
} else if (el.contentReference) { typeString = `<span class="text-info" title="Content Reference">Ref: ${escapeHtmlAttr(el.contentReference)}</span>`; }
else if (el.sliceName && path.endsWith('extension')) { typeString = '<span class="text-muted">Extension</span>'; }
// Children HTML / Choice Type Display (V18 logic - reverted primitive filtering)
let childrenOrChoiceHtml = '';
if (showToggle) {
childrenOrChoiceHtml += `<ul class="collapse list-group list-group-flush ms-3" id="${collapseId}">`;
// Render children if they exist OR if slicing defined
if (hasActualChildren || definesSlicing) {
const sortedChildren = (node.children || []).sort((a, b) => (a.name || '').localeCompare(b.name || ''));
sortedChildren.forEach(childNode => {
if (childNode) { childrenOrChoiceHtml += renderNodeAsLi(childNode, mustSupportPathsSet, resourceType, level + 1, view); }
});
}
// Render choice types ONLY if it's a choice element AND not sliced
else if (isChoiceElement && !definesSlicing && el.type && Array.isArray(el.type)) {
el.type.forEach(choiceType => {
const typeCode = choiceType.code || 'Unknown';
const baseName = (node.name || path.split('.').pop()).replace('[x]', '');
const choiceDisplayName = escapeHtmlAttr(baseName + typeCode.charAt(0).toUpperCase() + typeCode.slice(1));
const choiceTypeCode = escapeHtmlAttr(typeCode);
const targetProfiles = choiceType.targetProfile || choiceType.profile || [];
let profileDisplay = '';
if (targetProfiles.length > 0) {
const targetTypes = targetProfiles.map(p => (p || '').split('/').pop()).filter(Boolean).join(', ');
if (targetTypes) { profileDisplay = ` (<span class="fst-italic" title="Profile(s): ${escapeHtmlAttr(targetProfiles.join(', '))}">${escapeHtmlAttr(targetTypes)}</span>)`; }
}
const choiceTooltipAttr = definition ? `title="${escapeHtmlAttr(definition)}"` : '';
const choicePadding = padding + 20;
childrenOrChoiceHtml += `<li class="list-group-item py-1 border-0 choice-type-item" ${choiceTooltipAttr} ${definition ? 'data-bs-toggle="tooltip" data-bs-placement="top"' : ''}>` +
`<div class="row gx-2 align-items-center">` +
`<div class="col-lg-4 col-md-3 text-truncate" style="padding-left: ${choicePadding}px;"><code>${choiceDisplayName}</code></div>` +
`<div class="col-lg-1 col-md-1 text-center small"></div>` +
`<div class="col-lg-3 col-md-3 text-truncate small"><code>${choiceTypeCode}</code>${profileDisplay}</div>` +
`<div class="col-lg-1 col-md-1 text-center"></div>` +
`<div class="col-lg-3 col-md-4 text-truncate small">${escapeHtmlAttr(short)}</div>` +
`</div>` +
`</li>`;
});
}
childrenOrChoiceHtml += `</ul>`;
}
// Build list item HTML (Main row)
let itemHtml = `<li class="${liClass}" style="display: block;">`;
itemHtml += `<div class="row gx-2 align-items-center ${showToggle ? 'collapse-toggle' : ''}" ${showToggle ? `data-bs-toggle="collapse" data-bs-target="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}" style="cursor: pointer;"` : ''}>`;
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}">`;
itemHtml += `<span style="display: inline-block; width: 1.2em; text-align: center;">`;
if (showToggle) itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
itemHtml += `</span>`;
let displayName = escapeHtmlAttr(node.name);
if (definesSlicing && !isSlice) { displayName = `Slices for ${displayName}`; } // Use defined isSlice
const pathTitle = fhirPathExpression ? `${path} (FHIRPath: ${fhirPathExpression})` : path;
itemHtml += `<code class="fw-bold ms-1" title="${escapeHtmlAttr(pathTitle)}" data-bs-toggle="tooltip">${displayName}</code>`;
itemHtml += `</div>`;
const cardString = (min !== '' || max !== '') ? `${min}..${max}` : '';
itemHtml += `<div class="col-lg-1 col-md-1 text-center small"><code>${cardString}</code></div>`;
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
const descriptionTooltipAttr = definition ? `title="${escapeHtmlAttr(definition)}"` : '';
itemHtml += `<div class="col-lg-3 col-md-4 text-truncate small" ${descriptionTooltipAttr} ${definition ? 'data-bs-toggle="tooltip" data-bs-placement="top"' : ''}>${escapeHtmlAttr(short)}</div>`;
itemHtml += `</div>`; // End row
itemHtml += childrenOrChoiceHtml;
itemHtml += `</li>`;
return itemHtml;
} // End renderNodeAsLi (V19)
// --- Function to Initialize Collapse Icon Listeners (Bootstrap Events) V3 - Explicit Cleanup ---
function initializeBootstrapCollapseListeners(containerElement) {
if (!containerElement) { return; }
const viewName = containerElement.id.replace('structure-tree-', '');
const collapseElements = containerElement.querySelectorAll('.collapse');
collapseElements.forEach((collapseEl, index) => {
const collapseId = collapseEl.id; if (!collapseId) { return; }
const triggerEl = containerElement.querySelector(`[data-bs-target="#${collapseId}"]`);
const icon = triggerEl?.querySelector('.toggle-icon');
if (!triggerEl) { console.warn(`Trigger not found for #${collapseId}`); }
if (collapseEl._handleShow && typeof collapseEl._handleShow === 'function') { collapseEl.removeEventListener('show.bs.collapse', collapseEl._handleShow); }
if (collapseEl._handleHide && typeof collapseEl._handleHide === 'function') { collapseEl.removeEventListener('hide.bs.collapse', collapseEl._handleHide); }
const handleShow = (event) => { if (event.target === collapseEl && icon) { icon.classList.replace('bi-chevron-right', 'bi-chevron-down'); } /* console.log(...) */ };
const handleHide = (event) => { if (event.target === collapseEl && icon) { icon.classList.replace('bi-chevron-down', 'bi-chevron-right'); } /* console.log(...) */ };
collapseEl._handleShow = handleShow; collapseEl._handleHide = handleHide;
collapseEl.addEventListener('show.bs.collapse', handleShow);
collapseEl.addEventListener('hide.bs.collapse', handleHide);
if (icon) { /* Set initial icon */ if (collapseEl.classList.contains('show')) { icon.classList.replace('bi-chevron-right', 'bi-chevron-down'); } else { icon.classList.replace('bi-chevron-down', 'bi-chevron-right'); } }
});
} // End initializeBootstrapCollapseListeners V3
// --- Render Structure Tree (Calls V11 buildTreeData, V19 renderNode, V3 initBSCollapse) ---
async function renderStructureTree(resourceType, view, snapshotElements, mustSupportPaths) { // Accepts snapshot elements
const treeContainer = document.getElementById(`structure-tree-${view}`);
if (!treeContainer) { console.error(`Tree container missing: ${view}`); return; }
treeContainer.innerHTML = '';
try {
// Build tree using V11 buildTreeData and snapshot data
const treeData = buildTreeData(snapshotElements, view, mustSupportPaths);
// Render HTML using V19 renderNodeAsLi
let html = '<p class="ms-2 mb-1"><small>...</small></p>'; // Legend
html += '<ul class="list-group list-group-flush structure-tree-root">';
if (treeData.length > 0 && treeData[0]) {
html += renderNodeAsLi(treeData[0], mustSupportPaths, resourceType, 0, view); // V19 Call
} else { html += `<li class="list-group-item"><em class="text-muted">No elements match...</em></li>`; }
html += '</ul>';
treeContainer.innerHTML = html;
// Init listeners/tooltips
initializeBootstrapCollapseListeners(treeContainer); // V3
const tooltipTriggerList = treeContainer.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(tooltipTriggerEl => { const e=bootstrap.Tooltip.getInstance(tooltipTriggerEl); if(e){e.dispose();} if(tooltipTriggerEl.getAttribute('title')||tooltipTriggerEl.getAttribute('data-bs-original-title')){new bootstrap.Tooltip(tooltipTriggerEl);} });
} catch (error) {
console.error(`Error rendering ${view} tree:`, error);
treeContainer.innerHTML = `<div class="alert alert-danger m-2">Error rendering ${view}: ${error.message}</div>`;
}
} // End renderStructureTree
// --- Fetch Structure Definition for Specific View ---
async function fetchStructure(resourceType, view) {
const cacheKey = `${resourceType}-${view}`;
if (structureDataCache[cacheKey]) {
try { return JSON.parse(JSON.stringify(structureDataCache[cacheKey])); } catch (e) { return structureDataCache[cacheKey]; }
}
const structureBaseUrl = "{{ url_for('get_structure') }}";
const params = new URLSearchParams({ package_name: "{{ processed_ig.package_name }}", package_version: "{{ processed_ig.version }}", resource_type: resourceType, view: view });
const fetchUrl = `${structureBaseUrl}?${params.toString()}`;
try {
const response = await fetch(fetchUrl);
if (!response.ok) { throw new Error(`HTTP error ${response.status}`); }
const data = await response.json();
try { structureDataCache[cacheKey] = JSON.parse(JSON.stringify(data)); } catch(e) { structureDataCache[cacheKey] = data; }
return JSON.parse(JSON.stringify(data));
} catch (error) { console.error(`Error fetching ${view} structure:`, error); throw error; }
}
// --- Populate Structure for All Views - V4 (Add Search Param Logging) ---
async function populateStructure(resourceType) {
// ... (keep existing variable assignments and UI resets at the beginning) ...
if (structureTitle) structureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`;
if (rawStructureTitle) rawStructureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`;
if (structureLoading) structureLoading.style.display = 'block';
if (rawStructureLoading) rawStructureLoading.style.display = 'block';
if (structureDisplayWrapper) structureDisplayWrapper.style.display = 'block';
if (rawStructureWrapper) rawStructureWrapper.style.display = 'block';
if (structureFallbackMessage) { structureFallbackMessage.style.display = 'none'; structureFallbackMessage.textContent = ''; }
const views = ['differential', 'snapshot', 'must-support', 'key-elements'];
views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = ''; });
const constraintsTableBody = document.getElementById('constraints-table')?.querySelector('tbody');
const bindingsTableBody = document.getElementById('bindings-table')?.querySelector('tbody');
const searchParamsTableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
if (constraintsTableBody) constraintsTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted fst-italic">Loading...</td></tr>';
if (bindingsTableBody) bindingsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
if (searchParamsTableBody) searchParamsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
if (rawStructureContent) rawStructureContent.textContent = 'Loading...';
try {
console.log(`PopulateStructure V4: Fetching structure and search params for ${resourceType}`);
const structureBaseUrl = "{{ url_for('get_structure') }}";
const params = new URLSearchParams({ package_name: "{{ processed_ig.package_name }}", package_version: "{{ processed_ig.version }}", resource_type: resourceType });
const fetchUrl = `${structureBaseUrl}?${params.toString()}`;
const response = await fetch(fetchUrl);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error ${response.status}: ${errorText}`);
}
const data = await response.json();
if (!data || !data.elements) { throw new Error(`Structure data or elements missing for ${resourceType}.`); }
const snapshotElementsCopy = JSON.parse(JSON.stringify(data.elements));
const mustSupportPaths = new Set(data.must_support_paths || []);
const searchParams = data.search_parameters || []; // Get search params
const fallbackUsed = data.fallback_used || false;
const fallbackSource = data.source_package || 'FHIR core';
// *** ADDED LOG: Check received search parameters ***
console.log('[populateStructure] Received search_parameters:', searchParams);
// *************************************************
// Update Raw JSON View
if (rawStructureContent) {
rawStructureContent.textContent = JSON.stringify(data, null, 4);
if (rawStructureContent.classList.contains('hljs')) { rawStructureContent.classList.remove('hljs'); }
hljs.highlightElement(rawStructureContent);
if (fallbackUsed && structureFallbackMessage) {
structureFallbackMessage.textContent = `Note: Displaying structure from fallback package: ${fallbackSource}`;
structureFallbackMessage.style.display = 'block';
}
} else { if (rawStructureContent) rawStructureContent.textContent = '(Raw content display element not found)';}
// Render the Tree Views
for (const view of views) {
console.log(`PopulateStructure V4: Rendering view '${view}' for ${resourceType}`);
const treeContainer = document.getElementById(`structure-tree-${view}`);
if (!treeContainer) { console.error(`Container missing for ${view}`); continue; }
await renderStructureTree(resourceType, view, JSON.parse(JSON.stringify(snapshotElementsCopy)), mustSupportPaths);
}
// Render Constraints & Bindings tables
renderConstraintsTable(data.elements);
renderBindingsTable(data.elements);
// Render Search Params table (passing potentially empty array)
renderSearchParametersTable(searchParams); // Pass the retrieved data
} catch (error) {
console.error("Error populating structure:", error);
// ... existing error handling ...
views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = `<div class="alert alert-danger m-2">Error loading structure: ${error.message}</div>`; });
if (constraintsTableBody) constraintsTableBody.innerHTML = `<tr><td colspan="5" class="text-danger text-center">Error loading constraints: ${error.message}</td></tr>`;
if (bindingsTableBody) bindingsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading bindings: ${error.message}</td></tr>`;
if (searchParamsTableBody) searchParamsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading search params: ${error.message}</td></tr>`; // Show error here too
if (rawStructureContent) { rawStructureContent.textContent = `Error: ${error.message}`; }
} finally {
if (structureLoading) structureLoading.style.display = 'none';
if (rawStructureLoading) rawStructureLoading.style.display = 'none';
}
} // End populateStructure V4
// --- Populate Example Selector Dropdown ---
function populateExampleSelector(resourceOrProfileIdentifier) {
if (!exampleResourceTypeTitle || !exampleSelect || !exampleContentWrapper || !exampleFilename || !exampleContentRaw || !exampleContentJson || !exampleLoading || !exampleSelectorWrapper || !examplesData) { return; }
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
exampleSelect.innerHTML = '<option selected value="">-- Select an Example --</option>';
exampleContentWrapper.style.display = 'none'; exampleFilename.textContent = ''; exampleContentRaw.textContent = ''; exampleContentJson.textContent = ''; exampleLoading.style.display = 'none';
if (availableExamples.length > 0) {
availableExamples.forEach(filePath => { const f=filePath.split('/').pop(); const o=document.createElement('option'); o.value=filePath; o.textContent=f; exampleSelect.appendChild(o); });
exampleSelectorWrapper.style.display = 'block'; if (exampleDisplayWrapper) exampleDisplayWrapper.style.display = 'block';
} else { exampleSelectorWrapper.style.display = 'none'; if (exampleDisplayWrapper) exampleDisplayWrapper.style.display = 'none'; }
}
// --- Fetch and Display Selected Example ---
async function fetchAndDisplayExample(selectedFilePath) {
if (!exampleContentRaw || !exampleContentJson || !exampleFilename || !exampleContentWrapper || !exampleLoading) { return; }
exampleContentRaw.textContent = ''; exampleContentJson.textContent = ''; exampleFilename.textContent = ''; exampleContentWrapper.style.display = 'none'; exampleLoading.style.display = 'block';
const resetCopyButton = (button, tooltipInstance, originalTitle) => { if (tooltipInstance && button?.isConnected) { tooltipInstance.hide(); button.setAttribute('data-bs-original-title', originalTitle); button.querySelector('i')?.classList.replace('bi-check-lg', 'bi-clipboard'); } };
resetCopyButton(copyRawExButton, copyRawExTooltipInstance, 'Copy Raw Content'); resetCopyButton(copyPrettyJsonButton, copyPrettyJsonTooltipInstance, 'Copy JSON');
if (!selectedFilePath) { exampleLoading.style.display = 'none'; return; }
const exampleBaseUrl = "{{ url_for('get_example') }}"; const exampleParams = new URLSearchParams({ package_name: "{{ processed_ig.package_name }}", package_version: "{{ processed_ig.version }}", filename: selectedFilePath }); const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
try {
const response = await fetch(exampleFetchUrl); if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } const rawData = await response.text();
if(exampleFilename) exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`; if(exampleContentRaw) exampleContentRaw.textContent = rawData; let prettyJson = '(Not valid JSON)';
try { prettyJson = JSON.stringify(JSON.parse(rawData), null, 4); } catch (e) { prettyJson = rawData.startsWith('<') ? '(Content is XML, not JSON)' : `(Error parsing JSON: ${e.message})`; } if(exampleContentJson) exampleContentJson.textContent = prettyJson;
if (exampleContentRaw) hljs.highlightElement(exampleContentRaw); if (exampleContentJson) hljs.highlightElement(exampleContentJson); if(exampleContentWrapper) exampleContentWrapper.style.display = 'block';
} catch (error) { console.error("Error fetching or displaying example:", error); if (exampleFilename) exampleFilename.textContent = 'Error loading example'; if (exampleContentRaw) exampleContentRaw.textContent = `Error: ${error.message}`; if (exampleContentJson) exampleContentJson.textContent = `Error: ${error.message}`; if (exampleContentWrapper) exampleContentWrapper.style.display = 'block'; }
finally { if (exampleLoading) exampleLoading.style.display = 'none'; }
}
// --- Generic Copy Logic Function ---
function setupCopyButton(buttonElement, sourceElement, tooltipInstance, successTitle, originalTitle, errorTitle, nothingTitle) {
if (!buttonElement || !sourceElement) { return null; }
bootstrap.Tooltip.getInstance(buttonElement)?.dispose();
tooltipInstance = new bootstrap.Tooltip(buttonElement, { title: originalTitle });
const clickHandler = () => { const textToCopy = sourceElement.textContent; const copyIcon = buttonElement.querySelector('i'); const updateTooltip = (newTitle, iconClass) => { buttonElement.setAttribute('data-bs-original-title', newTitle); tooltipInstance?.setContent({ '.tooltip-inner': newTitle }); tooltipInstance?.show(); if (copyIcon) copyIcon.className = `bi ${iconClass}`; setTimeout(() => { if (buttonElement.isConnected) { tooltipInstance?.hide(); buttonElement.setAttribute('data-bs-original-title', originalTitle); if (copyIcon) copyIcon.className = 'bi bi-clipboard'; } }, 1500); }; if (textToCopy && textToCopy.trim() && !textToCopy.startsWith('(Could not') && !textToCopy.startsWith('(Not valid') && !textToCopy.startsWith('Error:')) { navigator.clipboard.writeText(textToCopy).then(() => { updateTooltip(successTitle, 'bi-check-lg'); }).catch(err => { console.error(`Copy failed:`, err); updateTooltip(errorTitle, 'bi-exclamation-triangle'); }); } else { updateTooltip(nothingTitle, 'bi-clipboard'); } };
buttonElement.removeEventListener('click', clickHandler); buttonElement.addEventListener('click', clickHandler); return tooltipInstance;
}
// --- Function to Render Constraints Table ---
function renderConstraintsTable(elements) {
const tableBody = document.getElementById('constraints-table')?.querySelector('tbody');
if (!tableBody) {
console.error('Constraints table body not found');
return;
}
tableBody.innerHTML = ''; // Clear existing rows
let constraintsFound = false;
if (!elements || !Array.isArray(elements)) {
console.warn("renderConstraintsTable called with invalid elements data:", elements);
elements = []; // Prevent errors later
}
elements.forEach(element => {
// Check if element exists and has a constraint property which is an array
if (element && element.constraint && Array.isArray(element.constraint) && element.constraint.length > 0) {
element.constraint.forEach(constraint => {
if (constraint) { // Ensure constraint object itself exists
constraintsFound = true;
const row = tableBody.insertRow();
row.insertCell().textContent = constraint.key || 'N/A';
row.insertCell().textContent = constraint.severity || 'N/A';
row.insertCell().textContent = element.path || 'N/A'; // Path of the element the constraint is on
row.insertCell().textContent = constraint.human || 'N/A';
const expressionCell = row.insertCell();
const codeElement = document.createElement('code');
codeElement.className = 'language-fhirpath'; // Use 'language-fhirpath' if you have a FHIRPath grammar for highlight.js
codeElement.textContent = constraint.expression || 'N/A';
expressionCell.appendChild(codeElement);
}
});
}
});
if (!constraintsFound) {
const row = tableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 5; // Match number of columns
cell.textContent = 'No constraints defined in this profile.';
cell.style.textAlign = 'center';
}
// Re-run highlight.js for the new code blocks
// Use setTimeout to allow the DOM to update before highlighting
setTimeout(() => {
document.querySelectorAll('#constraints-table code.language-fhirpath').forEach((block) => {
// Remove existing highlight classes if any, before re-highlighting
block.classList.remove('hljs');
hljs.highlightElement(block);
});
}, 0);
}
// --- Function to Render Terminology Bindings Table ---
function renderBindingsTable(elements) {
const tableBody = document.getElementById('bindings-table')?.querySelector('tbody');
if (!tableBody) {
console.error('Bindings table body not found');
return;
}
tableBody.innerHTML = ''; // Clear existing rows
let bindingsFound = false;
if (!elements || !Array.isArray(elements)) {
console.warn("renderBindingsTable called with invalid elements data:", elements);
elements = []; // Prevent errors later
}
elements.forEach(element => {
// Check if element exists and has a binding property which is an object
if (element && element.binding && typeof element.binding === 'object') {
bindingsFound = true;
const binding = element.binding;
const row = tableBody.insertRow();
row.insertCell().textContent = element.path || 'N/A';
row.insertCell().textContent = binding.strength || 'N/A';
const valueSetCell = row.insertCell();
if (binding.valueSet) {
const valueSetLink = document.createElement('a');
valueSetLink.href = binding.valueSet; // Ideally, resolve canonical to actual URL if possible
// Try to show last part of URL as name, or full URL if no '/'
const urlParts = binding.valueSet.split('/');
valueSetLink.textContent = urlParts.length > 1 ? urlParts[urlParts.length - 1] : binding.valueSet;
valueSetLink.target = '_blank'; // Open in new tab
valueSetLink.title = `Open ValueSet: ${binding.valueSet}`; // Add tooltip
valueSetLink.setAttribute('data-bs-toggle', 'tooltip'); // Enable tooltip
valueSetCell.appendChild(valueSetLink);
// Re-initialize tooltips for dynamically added links
const tooltip = new bootstrap.Tooltip(valueSetLink);
} else {
valueSetCell.textContent = 'N/A';
}
row.insertCell().textContent = binding.description || 'N/A';
}
});
if (!bindingsFound) {
const row = tableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 4; // Match number of columns
cell.textContent = 'No terminology bindings defined in this profile.';
cell.style.textAlign = 'center';
}
}
// --- Function to Render Search Parameters Table (Includes Conformance Badges & Tooltips) ---
function renderSearchParametersTable(searchParamsData) {
// Find the table body element using its ID
const tableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
// Check if the table body element exists
if (!tableBody) {
console.error('Search Params table body element (#searchparams-table > tbody) not found');
return; // Exit if the table body isn't found
}
tableBody.innerHTML = ''; // Clear any existing rows (like loading indicators)
// Log the data received for debugging
console.log('[renderSearchParametersTable] Received data:', searchParamsData);
// Check if the received data is valid and is an array with items
if (!searchParamsData || !Array.isArray(searchParamsData) || searchParamsData.length === 0) {
console.log('[renderSearchParametersTable] No valid search parameter data found, displaying message.');
// Display a message indicating no parameters were found
const row = tableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 4; // Span across all columns
cell.textContent = 'No relevant search parameters found for this resource type.';
cell.style.textAlign = 'center';
cell.classList.add('text-muted', 'fst-italic'); // Style the message
return; // Exit the function
}
// Sort parameters alphabetically by their 'code' before rendering
searchParamsData.sort((a, b) => (a.code || '').localeCompare(b.code || ''));
// Iterate through each search parameter object in the sorted array
searchParamsData.forEach((param, index) => {
console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param); // Log each parameter
const row = tableBody.insertRow(); // Create a new table row
// --- Column 1: Parameter Code ---
const nameCell = row.insertCell();
// Use escapeHtmlAttr (ensure this helper function exists in your scope)
// Display the parameter code within a <code> tag
nameCell.innerHTML = `<code>${escapeHtmlAttr(param.code || 'N/A')}</code>`;
// --- Column 2: Parameter Type ---
row.insertCell().textContent = param.type || 'N/A'; // Display the parameter type
// --- Column 3: Conformance Level (with Badges and Tooltip) ---
const conformanceCell = row.insertCell();
// Default to 'Optional' if conformance isn't specified in the data
const conformanceValue = param.conformance || 'Optional';
let badgeClass = 'bg-secondary'; // Default badge style
let titleText = 'Conformance level not specified in CapabilityStatement.'; // Default tooltip text
// Determine badge style and tooltip text based on the conformance value
if (conformanceValue === 'SHALL') {
badgeClass = 'bg-danger'; // Red for SHALL (Mandatory)
titleText = 'Server SHALL support this parameter.';
} else if (conformanceValue === 'SHOULD') {
badgeClass = 'bg-warning text-dark'; // Yellow for SHOULD (Recommended)
titleText = 'Server SHOULD support this parameter.';
} else if (conformanceValue === 'MAY') {
badgeClass = 'bg-info text-dark'; // Blue for MAY (Optional but defined)
titleText = 'Server MAY support this parameter.';
} else if (conformanceValue === 'Optional') {
badgeClass = 'bg-light text-dark border'; // Light grey for Optional (Not explicitly mentioned in Caps)
titleText = 'Server support for this parameter is optional.';
}
// Add handling for other potential values like 'SHALL-NOT', 'SHOULD-NOT' if needed
// Set the tooltip attributes
conformanceCell.setAttribute('title', titleText);
conformanceCell.setAttribute('data-bs-toggle', 'tooltip');
conformanceCell.setAttribute('data-bs-placement', 'top');
// Set the cell content with the styled badge
conformanceCell.innerHTML = `<span class="badge ${badgeClass}">${escapeHtmlAttr(conformanceValue)}</span>`;
// Initialize or re-initialize the Bootstrap tooltip for this specific cell
// This is important because the table rows are added dynamically
const existingTooltip = bootstrap.Tooltip.getInstance(conformanceCell);
if (existingTooltip) {
existingTooltip.dispose(); // Remove any previous tooltip instance to avoid conflicts
}
new bootstrap.Tooltip(conformanceCell); // Create and initialize the new tooltip
// --- Column 4: Description (with FHIRPath Tooltip if available) ---
const descriptionCell = row.insertCell();
// Use escapeHtmlAttr for the description text
descriptionCell.innerHTML = `<small>${escapeHtmlAttr(param.description || 'No description available.')}</small>`;
// If a FHIRPath expression exists, add it as a tooltip
if (param.expression) {
descriptionCell.setAttribute('title', `FHIRPath: ${param.expression}`);
descriptionCell.setAttribute('data-bs-toggle', 'tooltip');
descriptionCell.setAttribute('data-bs-placement', 'top');
// Initialize or re-initialize the tooltip
const existingDescTooltip = bootstrap.Tooltip.getInstance(descriptionCell);
if (existingDescTooltip) {
existingDescTooltip.dispose();
}
new bootstrap.Tooltip(descriptionCell);
}
}); // End forEach loop over searchParamsData
console.log('[renderSearchParametersTable] Finished rendering parameters.');
} // End renderSearchParametersTable function
// --- DOMContentLoaded Listener ---
document.addEventListener('DOMContentLoaded', function() {
try {
// --- Assign Global Variables to DOM Elements ---
structureDisplayWrapper = document.getElementById('structure-display-wrapper');
structureTitle = document.getElementById('structure-title');
structureLoading = document.getElementById('structure-loading');
structureFallbackMessage = document.getElementById('structure-fallback-message');
structureTreeDifferential = document.getElementById('structure-tree-differential');
structureTreeSnapshot = document.getElementById('structure-tree-snapshot');
structureTreeMustSupport = document.getElementById('structure-tree-must-support');
structureTreeKeyElements = document.getElementById('structure-tree-key-elements');
exampleDisplayWrapper = document.getElementById('example-display-wrapper');
exampleSelectorWrapper = document.getElementById('example-selector-wrapper');
exampleSelect = document.getElementById('example-select');
exampleResourceTypeTitle = document.getElementById('example-resource-type-title');
exampleLoading = document.getElementById('example-loading');
exampleContentWrapper = document.getElementById('example-content-wrapper');
exampleFilename = document.getElementById('example-filename');
exampleContentRaw = document.getElementById('example-content-raw');
exampleContentJson = document.getElementById('example-content-json');
rawStructureWrapper = document.getElementById('raw-structure-wrapper');
rawStructureTitle = document.getElementById('raw-structure-title');
rawStructureContent = document.getElementById('raw-structure-content');
rawStructureLoading = document.getElementById('raw-structure-loading');
copyRawDefButton = document.getElementById('copy-raw-def-button');
copyRawExButton = document.getElementById('copy-raw-ex-button');
copyPrettyJsonButton = document.getElementById('copy-pretty-json-button');
// --- Initialize Highlight.js ---
hljs.configure({ languages: ['json'] });
// --- Initialize Tooltips for static elements ---
const staticTooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
staticTooltips.forEach(t => { if (!bootstrap.Tooltip.getInstance(t)) { new bootstrap.Tooltip(t); } });
// --- Event Listener Setup ---
copyRawDefTooltipInstance = setupCopyButton(copyRawDefButton, rawStructureContent, copyRawDefTooltipInstance, 'Copied Definition!', 'Copy Raw Definition JSON', 'Copy Failed!', 'Nothing to copy');
copyRawExTooltipInstance = setupCopyButton(copyRawExButton, exampleContentRaw, copyRawExTooltipInstance, 'Copied Raw!', 'Copy Raw Content', 'Copy Failed!', 'Nothing to copy');
copyPrettyJsonTooltipInstance = setupCopyButton(copyPrettyJsonButton, exampleContentJson, copyPrettyJsonTooltipInstance, 'Copied JSON!', 'Copy JSON', 'Copy Failed!', 'Nothing to copy');
// Resource Type Link Click Handler
document.querySelectorAll('.resource-type-list').forEach(container => {
container.addEventListener('click', async function(event) {
const link = event.target.closest('.resource-type-link');
if (!link) return;
event.preventDefault();
const resourceType = link.dataset.resourceType;
if (!resourceType) { console.error("Missing data-resource-type"); return; }
console.log("Resource type link clicked:", resourceType);
await populateStructure(resourceType); // Uses V9 buildTreeData, V2 populateStructure
populateExampleSelector(resourceType);
if (structureDisplayWrapper) { structureDisplayWrapper.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
});
});
// Example Select Dropdown Change Handler
if (exampleSelect) {
exampleSelect.addEventListener('change', function(event) { fetchAndDisplayExample(this.value); });
} else { console.warn("Example select dropdown not found."); }
// --- No Collapse Listeners Here ---
// --- Initial UI State ---
if(structureDisplayWrapper) structureDisplayWrapper.style.display = 'none';
if(rawStructureWrapper) rawStructureWrapper.style.display = 'none';
if(exampleDisplayWrapper) exampleDisplayWrapper.style.display = 'none';
if(structureLoading) structureLoading.style.display = 'none';
if(rawStructureLoading) rawStructureLoading.style.display = 'none';
if(exampleLoading) exampleLoading.style.display = 'none';
if(structureFallbackMessage) structureFallbackMessage.style.display = 'none';
} catch (e) {
console.error("JavaScript error during DOMContentLoaded setup:", e);
const body = document.querySelector('body');
if (body) {
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger m-3';
errorDiv.textContent = 'A critical error occurred while initializing the page view.';
if(body.firstChild) { body.insertBefore(errorDiv, body.firstChild); } else { body.appendChild(errorDiv); }
}
}
}); // End DOMContentLoaded Listener
// --- END OF COMPLETE SCRIPT BLOCK ---
</script>
{% endblock %}