import React, { useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import localForage from 'localforage'; import { sourceLink, infoLine, ToggleDot } from './utils.js'; import Latex from 'react-latex-next'; import 'katex/dist/katex.min.css'; const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']; const DANGEROUS_TAGS = ['svg', 'math']; const latexDelimiters = [ { left: '$$', right: '$$', display: true }, { left: '\\[', right: '\\]', display: true }, { left: '\\(', right: '\\)', display: false } ]; function Article({ cache }) { const { id } = useParams(); if (id in cache) console.log('cache hit'); const [story, setStory] = useState(cache[id] || false); const [error, setError] = useState(''); const [pConv, setPConv] = useState([]); const [copyButtonText, setCopyButtonText] = useState('\ue92c'); useEffect(() => { localForage.getItem(id) .then( (value) => { if (value) { setStory(value); } } ); fetch('/api/' + id) .then(res => { if (!res.ok) { throw new Error(`Server responded with ${res.status} ${res.statusText}`); } return res.json(); }) .then( (result) => { setStory(result.story); localForage.setItem(id, result.story); }, (error) => { const errorMessage = `Failed to fetch new article content (ID: ${id}). Your connection may be down or the server might be experiencing issues. ${error.toString()}.`; setError(errorMessage); } ); }, [id]); const copyLink = () => { navigator.clipboard.writeText(`${story.title}:\n${window.location.href}`).then(() => { setCopyButtonText('\uea10'); setTimeout(() => setCopyButtonText('\ue92c'), 2000); }, () => { setCopyButtonText('\uea0f'); setTimeout(() => setCopyButtonText('\ue92c'), 2000); }); }; const pConvert = (n) => { setPConv(prevPConv => [...prevPConv, n]); }; const isCodeBlock = (v) => { if (v.localName === 'pre') { return true; } if (v.localName === 'code') { if (v.closest('p')) { return false; } const parent = v.parentElement; if (parent) { const nonWhitespaceChildren = Array.from(parent.childNodes).filter(n => { return n.nodeType !== Node.TEXT_NODE || n.textContent.trim() !== ''; }); if (nonWhitespaceChildren.length === 1 && nonWhitespaceChildren[0] === v) { return true; } } } return false; }; const renderNodes = (nodes, keyPrefix = '') => { return Array.from(nodes).map((v, k) => { const key = `${keyPrefix}${k}`; if (pConv.includes(key)) { return ( {v.textContent.split('\n\n').map((x, i) =>

{x}

)}
); } if (v.nodeName === '#text') { const text = v.data; if (text.includes('\\[') || text.includes('\\(') || text.includes('$$')) { return {text}; } // Only wrap top-level text nodes in

if (keyPrefix === '' && v.data.trim() !== '') { return

{v.data}

; } return v.data; } if (v.nodeType !== Node.ELEMENT_NODE) { return null; } if (DANGEROUS_TAGS.includes(v.localName)) { return ; } const Tag = v.localName; if (isCodeBlock(v)) { return ( ); } const textContent = v.textContent.trim(); const isMath = (textContent.startsWith('\\(') && textContent.endsWith('\\)')) || (textContent.startsWith('\\[') && textContent.endsWith('\\]')) || (textContent.startsWith('$$') && textContent.endsWith('$$')); const props = { key: key }; if (v.hasAttributes()) { for (const attr of v.attributes) { const name = attr.name === 'class' ? 'className' : attr.name; props[name] = attr.value; } } if (isMath) { let mathContent = v.textContent; // align environment requires display math mode if (mathContent.includes('\\begin{align')) { const trimmed = mathContent.trim(); if (trimmed.startsWith('\\(')) { // Replace \( and \) with \[ and \] to switch to display mode const firstParen = mathContent.indexOf('\\('); const lastParen = mathContent.lastIndexOf('\\)'); mathContent = mathContent.substring(0, firstParen) + '\\[' + mathContent.substring(firstParen + 2, lastParen) + '\\]' + mathContent.substring(lastParen + 2); } } return {mathContent}; } if (VOID_ELEMENTS.includes(Tag)) { return ; } return ( {renderNodes(v.childNodes, `${key}-`)} ); }); }; const nodes = (s) => { if (s && s.text) { let div = document.createElement('div'); div.innerHTML = s.text; return div.childNodes; } return null; }; const storyNodes = nodes(story); return (
{error &&
Connection error? Click to expand.

{error}

{story &&

Loaded article from cache.

}
} {story ?
{story.title} | QotNews

{story.title}

Source: {sourceLink(story)}
{infoLine(story)} {storyNodes ?
{renderNodes(storyNodes)}
:

Problem getting article :(

}
:

Loading...

}
); } export default Article;