Fix Turtle parser multi-line string handling for PiCo ontology

- Fixed bug where closing triple-quotes (""") would incorrectly re-trigger
  multi-line string detection, causing subsequent class definitions to be skipped
- Added lineToProcess variable to track which portion of line to process after
  closing a multi-line string, preventing re-detection of opening quotes
- Moved UML large diagram confirmation logic from OntologyViewerPage to
  UMLVisualization component for better encapsulation
- PiCo ontology now correctly shows all 8 classes instead of 2

Deployed and verified on https://bronhouder.nl/ontology?ontology=PiCo
This commit is contained in:
kempersc 2026-01-05 11:25:43 +01:00
parent 242bc8bb35
commit 41d8905661
6 changed files with 141 additions and 130 deletions

View file

@ -1,5 +1,5 @@
{
"generated": "2026-01-04T22:18:27.627Z",
"generated": "2026-01-05T08:03:59.341Z",
"version": "1.0.0",
"categories": [
{

View file

@ -153,6 +153,35 @@
background: #f9fafb;
}
.uml-visualization__search-count {
padding: 0.5rem 0.85rem;
font-size: 0.7rem;
color: #6b7280;
background: #f3f4f6;
border-bottom: 1px solid #e5e7eb;
text-align: center;
}
.uml-visualization__search-more-btn {
display: block;
width: 100%;
padding: 0.6rem 0.85rem;
font-size: 0.75rem;
color: #0a3dfa;
font-weight: 500;
text-align: center;
background: #f9fafb;
border: none;
border-top: 1px solid #e5e7eb;
cursor: pointer;
transition: all 0.15s;
}
.uml-visualization__search-more-btn:hover {
background: #eff6ff;
color: #0832d1;
}
.uml-visualization__zoom {
font-size: 0.6875rem;
color: #666;
@ -738,4 +767,21 @@
border-top-color: #374151;
background: #111827;
}
.uml-visualization__search-count {
color: #9ca3af;
background: #111827;
border-bottom-color: #374151;
}
.uml-visualization__search-more-btn {
color: #818cf8;
background: #111827;
border-top-color: #374151;
}
.uml-visualization__search-more-btn:hover {
background: #1e3a5f;
color: #a5b4fc;
}
}

View file

@ -290,6 +290,7 @@ const UMLVisualizationInner: React.FC<UMLVisualizationProps> = ({
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<UMLNode[]>([]);
const [showSearchResults, setShowSearchResults] = useState(false);
const [visibleResultsCount, setVisibleResultsCount] = useState(10);
const [showProvenanceLinks, setShowProvenanceLinks] = useState(false); // Provenance links hidden by default
const [showLegend, setShowLegend] = useState(false); // Cardinality legend dropdown
const [edgeTooltip, setEdgeTooltip] = useState<{ label: string; x: number; y: number; bidirectional: boolean; containerWidth: number; containerHeight: number } | null>(null);
@ -2275,6 +2276,7 @@ const UMLVisualizationInner: React.FC<UMLVisualizationProps> = ({
// Search handler - filter nodes based on query
const handleSearch = (query: string) => {
setSearchQuery(query);
setVisibleResultsCount(10); // Reset visible count on new search
if (query.trim() === '') {
setSearchResults([]);
setShowSearchResults(false);
@ -2359,7 +2361,12 @@ const UMLVisualizationInner: React.FC<UMLVisualizationProps> = ({
/>
{showSearchResults && searchResults.length > 0 && (
<div className="uml-visualization__search-results">
{searchResults.slice(0, 10).map(node => (
{searchResults.length > visibleResultsCount && (
<div className="uml-visualization__search-count">
Showing {visibleResultsCount} of {searchResults.length} results
</div>
)}
{searchResults.slice(0, visibleResultsCount).map(node => (
<button
key={node.id}
className="uml-visualization__search-result"
@ -2369,9 +2376,20 @@ const UMLVisualizationInner: React.FC<UMLVisualizationProps> = ({
<span className="uml-visualization__search-result-type">«{node.type}»</span>
</button>
))}
{searchResults.length > 10 && (
{searchResults.length > visibleResultsCount && visibleResultsCount < 50 && (
<button
className="uml-visualization__search-more-btn"
onMouseDown={(e) => {
e.preventDefault(); // Prevent blur from closing dropdown
setVisibleResultsCount(prev => Math.min(prev + 10, 50));
}}
>
Show more ({Math.min(10, searchResults.length - visibleResultsCount)} more)
</button>
)}
{visibleResultsCount >= 50 && searchResults.length > 50 && (
<div className="uml-visualization__search-more">
+{searchResults.length - 10} more results
+{searchResults.length - 50} more results (max 50 shown)
</div>
)}
</div>

View file

@ -621,6 +621,8 @@ function parseTurtleOntology(content: string): ParsedOntology {
let currentTriples: Array<{ predicate: string; object: string }> = [];
let lastPredicate: string | null = null; // Track last predicate for comma continuations
let blankNodeDepth = 0; // Track depth of blank node blocks to skip
let inMultiLineString = false; // Track if we're inside a multi-line triple-quoted string
let multiLineQuoteChar = ''; // The quote character(s) for the multi-line string
// First pass: extract prefixes
for (const line of lines) {
@ -634,18 +636,76 @@ function parseTurtleOntology(content: string): ParsedOntology {
}
// Second pass: parse triples
let lineNum = 0;
for (const line of lines) {
lineNum++;
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('@prefix') || trimmed.startsWith('PREFIX')) {
continue;
}
// Track blank node depth across lines
// Handle multi-line triple-quoted strings
// If we're inside a multi-line string, check if this line closes it
let lineToProcess = trimmed; // The portion of the line to process for brackets
if (inMultiLineString) {
const closeIndex = trimmed.indexOf(multiLineQuoteChar);
if (closeIndex !== -1) {
// Found the closing triple-quote, process rest of line after it
inMultiLineString = false;
const afterQuote = trimmed.substring(closeIndex + 3);
// Count brackets in the part after the closing quotes
for (const char of afterQuote) {
if (char === '[') blankNodeDepth++;
else if (char === ']') blankNodeDepth--;
}
// If the line after closing quote is just "] ;" or similar, skip it
if (afterQuote.trim().match(/^[\]\s;,.]*$/)) {
continue;
}
// IMPORTANT: Only process the part AFTER the closing quote for bracket detection
// Otherwise we'll re-detect the opening """ and incorrectly enter multi-line mode
lineToProcess = afterQuote;
} else {
// Still inside the multi-line string, skip this line entirely
continue;
}
}
// Track blank node depth across lines BEFORE processing
// We need to track this first to know if we should skip this line
const prevBlankNodeDepth = blankNodeDepth;
// Count opening and closing brackets (outside quotes)
// IMPORTANT: Use lineToProcess (not trimmed) to avoid re-detecting """ after closing a multi-line string
let inQuotes = false;
for (const char of trimmed) {
if (char === '"' || char === "'") {
inQuotes = !inQuotes;
let quoteChar = '';
for (let i = 0; i < lineToProcess.length; i++) {
const char = lineToProcess[i];
if (!inQuotes && (char === '"' || char === "'")) {
// Check for triple-quoted strings
if (lineToProcess.substring(i, i + 3) === '"""' || lineToProcess.substring(i, i + 3) === "'''") {
// Check if the closing triple-quote is on the same line
const restOfLine = lineToProcess.substring(i + 3);
const closeIndex = restOfLine.indexOf(lineToProcess.substring(i, i + 3));
if (closeIndex === -1) {
// Triple-quote opens but doesn't close on this line - multi-line string
inMultiLineString = true;
multiLineQuoteChar = lineToProcess.substring(i, i + 3);
break; // Stop processing brackets on this line
} else {
// Triple-quote opens and closes on same line, skip to after closing
i += 3 + closeIndex + 2; // +3 for opening, +closeIndex to reach close, +2 to skip close
}
} else {
inQuotes = true;
quoteChar = char;
}
} else if (inQuotes) {
// Check for end of single-quoted string
if (quoteChar.length === 1 && char === quoteChar && lineToProcess[i - 1] !== '\\') {
inQuotes = false;
quoteChar = '';
}
} else if (!inQuotes) {
if (char === '[') blankNodeDepth++;
else if (char === ']') blankNodeDepth--;
@ -653,12 +713,16 @@ function parseTurtleOntology(content: string): ParsedOntology {
}
// Skip lines that are entirely within a blank node block
// (but process lines that start or end a blank node block for proper depth tracking)
if (blankNodeDepth > 0 && !trimmed.includes('[') && !trimmed.includes(']')) {
// A line is "inside" a blank node if:
// 1. We started inside (prevBlankNodeDepth > 0) and we're still inside (blankNodeDepth > 0)
// 2. OR we're on a line that only closes brackets
if (prevBlankNodeDepth > 0 && blankNodeDepth >= 0 && !trimmed.match(/^[a-zA-Z_]/)) {
// We're inside a blank node block and this line doesn't start a new subject
continue;
}
// Also skip lines that only close a blank node
if (trimmed === ']' || trimmed === '] ;' || trimmed === '] ,' || trimmed === '] .') {
// Skip lines that only contain blank node closure
if (trimmed.match(/^[\]\s;,.]+$/)) {
continue;
}

View file

@ -760,72 +760,6 @@
font-size: 1rem;
}
/* UML Large Diagram Warning */
.ontology-viewer__uml-warning {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 3rem 2rem;
min-height: 400px;
background: var(--surface-color, #fff);
border-radius: 8px;
text-align: center;
}
.ontology-viewer__uml-warning-icon {
font-size: 3rem;
line-height: 1;
}
.ontology-viewer__uml-warning h3 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary, #212121);
}
.ontology-viewer__uml-warning-stats {
margin: 0;
font-size: 1.125rem;
color: var(--text-secondary, #757575);
}
.ontology-viewer__uml-warning-stats strong {
color: var(--primary-color, #1976d2);
font-weight: 600;
}
.ontology-viewer__uml-warning-text {
margin: 0;
max-width: 400px;
font-size: 0.95rem;
color: var(--text-secondary, #757575);
line-height: 1.5;
}
.ontology-viewer__uml-warning-button {
margin-top: 0.5rem;
padding: 0.75rem 1.5rem;
background: var(--primary-color, #1976d2);
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s ease, transform 0.1s ease;
}
.ontology-viewer__uml-warning-button:hover {
background: var(--primary-dark, #1565c0);
}
.ontology-viewer__uml-warning-button:active {
transform: scale(0.98);
}
/* Responsive Design */
@media (max-width: 1024px) {
.ontology-viewer-page {

View file

@ -47,16 +47,6 @@ import { UMLVisualization, type UMLDiagram, type UMLNode, type UMLLink } from '.
import './OntologyViewerPage.css';
import '../styles/collapsible.css';
/**
* Performance thresholds for UML diagram rendering.
* Large ontologies (Schema.org, CIDOC-CRM, RiC-O) can freeze the browser.
* Show warning and require user confirmation before rendering large diagrams.
*/
const UML_LARGE_DIAGRAM_THRESHOLD = {
nodes: 50, // Number of classes
links: 100, // Number of relationships
};
/**
* Normalize URI for comparison by handling http vs https variants.
* Schema.org in particular uses https internally but is often referenced via http.
@ -371,12 +361,7 @@ const TEXT = {
collapseSidebar: { nl: 'Zijbalk inklappen', en: 'Collapse sidebar' },
expandSidebar: { nl: 'Zijbalk uitklappen', en: 'Expand sidebar' },
// UML diagram performance warnings
largeDiagramDetected: { nl: 'Grote Ontologie Gedetecteerd', en: 'Large Ontology Detected' },
largeDiagramStats: { nl: 'klassen en', en: 'classes and' },
largeDiagramRelationships: { nl: 'relaties', en: 'relationships' },
largeDiagramWarning: { nl: 'Het renderen kan enkele seconden duren en kan uw browser vertragen.', en: 'Rendering may take several seconds and could slow your browser.' },
loadDiagramAnyway: { nl: 'Toch Laden', en: 'Load Diagram Anyway' },
// UML diagram
umlLoading: { nl: 'UML-diagram genereren...', en: 'Generating UML diagram...' },
};
@ -753,9 +738,6 @@ const OntologyViewerPage: React.FC = () => {
// State for sidebar collapse
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
// State for UML large diagram confirmation (performance optimization)
const [umlLoadConfirmed, setUmlLoadConfirmed] = useState<boolean>(false);
// Track if we need to scroll after loading completes
const pendingScrollRef = useRef<string | null>(null);
@ -825,10 +807,6 @@ const OntologyViewerPage: React.FC = () => {
useEffect(() => {
if (!selectedOntology) return;
// Reset UML load confirmation when ontology changes
// (user must re-confirm for each large ontology)
setUmlLoadConfirmed(false);
const loadSelectedOntology = async () => {
setIsLoading(true);
setError(null);
@ -1540,10 +1518,6 @@ const OntologyViewerPage: React.FC = () => {
/**
* Render UML diagram view of the ontology
*
* Performance optimization: Large ontologies (Schema.org, CIDOC-CRM, RiC-O)
* can freeze the browser. We show a warning and require user confirmation
* before rendering large diagrams.
*/
const renderUMLView = () => {
if (!ontology || !umlDiagram) return null;
@ -1557,31 +1531,6 @@ const OntologyViewerPage: React.FC = () => {
);
}
// Check if this is a large diagram that could freeze the browser
const isLargeDiagram =
umlDiagram.nodes.length > UML_LARGE_DIAGRAM_THRESHOLD.nodes ||
umlDiagram.links.length > UML_LARGE_DIAGRAM_THRESHOLD.links;
// Show warning for large diagrams - require user confirmation
if (isLargeDiagram && !umlLoadConfirmed) {
return (
<div className="ontology-viewer__uml-warning">
<div className="ontology-viewer__uml-warning-icon"></div>
<h3>{t('largeDiagramDetected')}</h3>
<p className="ontology-viewer__uml-warning-stats">
<strong>{umlDiagram.nodes.length}</strong> {t('largeDiagramStats')} <strong>{umlDiagram.links.length}</strong> {t('largeDiagramRelationships')}
</p>
<p className="ontology-viewer__uml-warning-text">{t('largeDiagramWarning')}</p>
<button
className="ontology-viewer__uml-warning-button"
onClick={() => setUmlLoadConfirmed(true)}
>
{t('loadDiagramAnyway')}
</button>
</div>
);
}
return (
<div className="ontology-viewer__uml-container">
<UMLVisualization