Compare commits
10 Commits
6dc47f6672
...
92e70229fe
| Author | SHA1 | Date | |
|---|---|---|---|
| 92e70229fe | |||
| b749e58f62 | |||
| b1b2be6080 | |||
| 5ebe87dbc2 | |||
| a8a36b693e | |||
| 60eefb4b27 | |||
| 8f5dae4bdc | |||
| 89a511efc0 | |||
| 504fe745ea | |||
| 762e8a9a2e |
@@ -1,9 +1,11 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import localForage from 'localforage';
|
import localForage from 'localforage';
|
||||||
import { sourceLink, infoLine, ToggleDot } from './utils.js';
|
import { sourceLink, infoLine, ToggleDot } from './utils.js';
|
||||||
|
|
||||||
|
const VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
||||||
|
|
||||||
function Article({ cache }) {
|
function Article({ cache }) {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
@@ -58,17 +60,92 @@ function Article({ cache }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isCodeBlock = (v) => {
|
const isCodeBlock = (v) => {
|
||||||
return v.localName === 'pre' || v.localName === 'code' || (v.children?.length === 1 && v.children[0].localName === 'code');
|
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 nodes = useMemo(() => {
|
const renderNodes = (nodes, keyPrefix = '') => {
|
||||||
if (story && story.text) {
|
return Array.from(nodes).map((v, k) => {
|
||||||
|
const key = `${keyPrefix}${k}`;
|
||||||
|
if (pConv.includes(key)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={key}>
|
||||||
|
{v.textContent.split('\n\n').map((x, i) =>
|
||||||
|
<p key={i}>{x}</p>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.nodeName === '#text') {
|
||||||
|
// Only wrap top-level text nodes in <p>
|
||||||
|
if (keyPrefix === '' && v.data.trim() !== '') {
|
||||||
|
return <p key={key}>{v.data}</p>;
|
||||||
|
}
|
||||||
|
return v.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.nodeType !== Node.ELEMENT_NODE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tag = v.localName;
|
||||||
|
if (isCodeBlock(v)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={key}>
|
||||||
|
<Tag dangerouslySetInnerHTML={{ __html: v.innerHTML }} />
|
||||||
|
<button onClick={() => pConvert(key)}>Convert Code to Paragraph</button>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (VOID_ELEMENTS.includes(Tag)) {
|
||||||
|
return <Tag {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag {...props}>
|
||||||
|
{renderNodes(v.childNodes, `${key}-`)}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodes = (s) => {
|
||||||
|
if (s && s.text) {
|
||||||
let div = document.createElement('div');
|
let div = document.createElement('div');
|
||||||
div.innerHTML = story.text;
|
div.innerHTML = s.text;
|
||||||
return div.childNodes;
|
return div.childNodes;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [story]);
|
};
|
||||||
|
|
||||||
|
const storyNodes = nodes(story);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='article-container'>
|
<div className='article-container'>
|
||||||
@@ -94,25 +171,9 @@ function Article({ cache }) {
|
|||||||
|
|
||||||
{infoLine(story)}
|
{infoLine(story)}
|
||||||
|
|
||||||
{nodes ?
|
{storyNodes ?
|
||||||
<div className='story-text'>
|
<div className='story-text'>
|
||||||
{Object.entries(nodes).map(([k, v]) =>
|
{renderNodes(storyNodes)}
|
||||||
pConv.includes(k) ?
|
|
||||||
<React.Fragment key={k}>
|
|
||||||
{v.innerHTML.split('\n\n').map((x, i) =>
|
|
||||||
<p key={i} dangerouslySetInnerHTML={{ __html: x }} />
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
:
|
|
||||||
(v.nodeName === '#text' ?
|
|
||||||
<p key={k}>{v.data}</p>
|
|
||||||
:
|
|
||||||
<React.Fragment key={k}>
|
|
||||||
<v.localName dangerouslySetInnerHTML={v.innerHTML ? { __html: v.innerHTML } : null} />
|
|
||||||
{isCodeBlock(v) && <button onClick={() => pConvert(k)}>Convert Code to Paragraph</button>}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<p>Problem getting article :(</p>
|
<p>Problem getting article :(</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user