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:
parent
242bc8bb35
commit
41d8905661
6 changed files with 141 additions and 130 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"generated": "2026-01-04T22:18:27.627Z",
|
||||
"generated": "2026-01-05T08:03:59.341Z",
|
||||
"version": "1.0.0",
|
||||
"categories": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue