diff --git a/package.json b/package.json index 2518a1c1..b416aec2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "i18next": "^19.8.4", "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.1.1", + "iconoir-react": "^6.11.0", "jsonschema": "^1.3.0", "lodash.find": "^4.6.0", "lodash.findindex": "^4.6.0", diff --git a/src/components/Article/ArticleCell.js b/src/components/Article/ArticleCell.js index ea886e2c..64fab0b2 100644 --- a/src/components/Article/ArticleCell.js +++ b/src/components/Article/ArticleCell.js @@ -232,9 +232,8 @@ const ArticleCell = ({ return
unknown type: {type}
} -export default React.memo(ArticleCell, (prevProps, nextProps) => { - if (prevProps.memoid === nextProps.memoid || prevProps.active === nextProps.active) { - return true // props are equal - } - return false // props are not equal -> update the component -}) +export default React.memo( + ArticleCell, + (prevProps, nextProps) => + prevProps.memoid === nextProps.memoid || prevProps.active === nextProps.active, +) diff --git a/src/components/Article/ArticleCellFigure.js b/src/components/Article/ArticleCellFigure.js index 5cdf31fa..e9c24e7d 100644 --- a/src/components/Article/ArticleCellFigure.js +++ b/src/components/Article/ArticleCellFigure.js @@ -1,6 +1,6 @@ import React, { useMemo } from 'react' import ArticleCellOutputs from './ArticleCellOutputs' -import ArticleFigure from './ArticleFigure' +import ArticleFigureCaption from './ArticleFigureCaption' import { markdownParser } from '../../logic/ipynb' import { BootstrapColumLayout } from '../../constants' import { Container, Row, Col } from 'react-bootstrap' @@ -61,13 +61,18 @@ const ArticleCellFigure = ({ } const mimetypes = Object.keys(output.data ?? []) const mimetype = mimetypes.find((d) => d.indexOf('image/') === 0) + const outputProps = output ? Object.keys(output) : [] + const isOutputEmpty = + outputProps.length === 0 || + (outputProps.length === 1 && outputProps.shift() === 'metadata') + if (mimetype) { acc.pictures.push({ // ...output, src: output.metadata?.jdh?.object?.src, base64: `data:${mimetype};base64,${output.data[mimetype]}`, }) - } else { + } else if (!isOutputEmpty) { acc.otherOutputs.push(output) } return acc @@ -101,7 +106,7 @@ const ArticleCellFigure = ({ ) return ( -
+
@@ -164,15 +169,15 @@ const ArticleCellFigure = ({ {sourceCode} - -

+

') .replace(/(Fig.|figure|table)\s+[\da-z-]+\s*:\s+/i, ''), }} /> - + diff --git a/src/components/Article/ArticleCellObject.js b/src/components/Article/ArticleCellObject.js index c66ef939..a188b1e2 100644 --- a/src/components/Article/ArticleCellObject.js +++ b/src/components/Article/ArticleCellObject.js @@ -1,11 +1,10 @@ import React, { useMemo, lazy } from 'react' import { markdownParser } from '../../logic/ipynb' -import ArticleFigure from './ArticleFigure' +import ArticleFigureCaption from './ArticleFigureCaption' const VegaWrapper = lazy(() => import('../Module/VegaWrapper')) const ImageWrapper = lazy(() => import('../Module/ImageWrapper')) - const ArticleCellObject = ({ metadata, figure, children, progress }) => { const objectMetadata = useMemo(() => metadata.jdh?.object ?? {}, [metadata.jdh]) const objectOutputs = useMemo(() => metadata.jdh?.outputs ?? [], [metadata.jdh]) @@ -30,16 +29,21 @@ const ArticleCellObject = ({ metadata, figure, children, progress }) => { alignItems: 'center', } // flex alignment - if ([ 'start', 'flex-start', 'end', 'flex-end', 'center', 'space-between', 'space-around'].includes(objectMetadata.justifyContent)) { + if ( + ['start', 'flex-start', 'end', 'flex-end', 'center', 'space-between', 'space-around'].includes( + objectMetadata.justifyContent, + ) + ) { objectWrapperStyle = { ...objectWrapperStyle, justifyContent: objectMetadata.justifyContent, } } - if ([ - 'start', 'flex-start', 'end', 'flex-end', 'center', - 'baseline', 'first baseline' - ].includes(objectMetadata.alignItems)) { + if ( + ['start', 'flex-start', 'end', 'flex-end', 'center', 'baseline', 'first baseline'].includes( + objectMetadata.alignItems, + ) + ) { objectWrapperStyle = { ...objectWrapperStyle, alignItems: objectMetadata.alignItems, @@ -49,7 +53,7 @@ const ArticleCellObject = ({ metadata, figure, children, progress }) => { if (!isNaN(objectMetadata.heightRatio)) { objectWrapperStyle = { ...objectWrapperStyle, - height: window.innerHeight * objectMetadata.heightRatio + height: window.innerHeight * objectMetadata.heightRatio, } } @@ -59,44 +63,38 @@ const ArticleCellObject = ({ metadata, figure, children, progress }) => { position: 'sticky', top: objectMetadata.top ?? 'var(--spacer-3)', } - if(isNaN(objectMetadata.heightRatio)) { + if (isNaN(objectMetadata.heightRatio)) { objectWrapperStyle.height = 'auto' } } - return (<> -
- {objectMetadata.type === 'image' && objectOutputs.map((output, i) => ( - - ))} - {['video', 'map'].includes(objectMetadata.type) && ( - <> - {progress} -
- - )} - {['vega'].includes(objectMetadata.type) - ? ( - - -
-
-
) - : null - } - {['text'].includes(objectMetadata.type) && ( -
- )} -
-
{children}
+ return ( + <> +
+ {objectMetadata.type === 'image' && + objectOutputs.map((output, i) => ( + + ))} + {['video', 'map'].includes(objectMetadata.type) && ( + <> + {progress} +
+ + )} + {['vega'].includes(objectMetadata.type) ? ( + + +
+
+
+ ) : null} + {['text'].includes(objectMetadata.type) && ( +
+ )} +
+
{children}
) } - export default ArticleCellObject diff --git a/src/components/Article/ArticleFigure.js b/src/components/Article/ArticleFigure.js deleted file mode 100644 index 54b69639..00000000 --- a/src/components/Article/ArticleFigure.js +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react' -import { useTranslation } from 'react-i18next' - -const ArticleFigure = ({ figure, children }) => { - const { t } = useTranslation() - // figure number, for translation label. - return ( -
-
- { figure.ref - ? ( - - {t(figure.tNLabel, { n: figure.tNum })} - - ) - : ( -
- {t(figure.tNLabel, { n: figure.tNum })} -
- ) - } -
- {children} -
- ) -} - -export default ArticleFigure diff --git a/src/components/Article/ArticleFigureCaption.js b/src/components/Article/ArticleFigureCaption.js new file mode 100644 index 00000000..ee69d8db --- /dev/null +++ b/src/components/Article/ArticleFigureCaption.js @@ -0,0 +1,27 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { DisplayLayerCellIdxQueryParam } from '../../constants' +import { useQueryParam, NumberParam } from 'use-query-params' +import './ArticleFigureCaption.scss' + +const ArticleFigureCaption = ({ figure, className = '', children }) => { + const { t } = useTranslation() + const [, set] = useQueryParam(DisplayLayerCellIdxQueryParam, NumberParam) + // figure number, for translation label. + return ( +
+
+ {figure.ref ? ( + + ) : ( +
{t(figure.tNLabel, { n: figure.tNum })}
+ )} +
+ {children} +
+ ) +} + +export default ArticleFigureCaption diff --git a/src/components/Article/ArticleFigureCaption.scss b/src/components/Article/ArticleFigureCaption.scss new file mode 100644 index 00000000..67bda668 --- /dev/null +++ b/src/components/Article/ArticleFigureCaption.scss @@ -0,0 +1,29 @@ +.ArticleFigure { + min-height: 50px; + margin-top: var(--spacer-3); +} + +.ArticleFigure__figcaption_num { + position: absolute; + left: -140px; + width: 140px; + text-align: right; + padding-right: var(--spacer-3); + font-family: var(--font-family-monospace); + font-weight: bold; +} + +.ArticleFigure button.btn-link { + color: #000; + border-radius: 0; + font-size: inherit; + padding: 0; + line-height: 1.05em; + text-decoration: none; + box-shadow: 0 1px 0 var(--secondary); + font-weight: bold; + &:hover { + text-decoration: none; + color: #000; + } +} diff --git a/src/components/Article/ArticleScrollamaSticky.js b/src/components/Article/ArticleScrollamaSticky.js index 5ff4d947..7dad1861 100644 --- a/src/components/Article/ArticleScrollamaSticky.js +++ b/src/components/Article/ArticleScrollamaSticky.js @@ -1,10 +1,10 @@ import React from 'react' import { Container, Row, Col } from 'react-bootstrap' -import ArticleFigure from './ArticleFigure' +import ArticleFigureCaption from './ArticleFigureCaption' import ArticleCellOutput from './ArticleCellOutput' import { BootstrapNarrativeStepFigureColumnLayout, - BootstrapNarrativeStepCaptionColumnLayout + BootstrapNarrativeStepCaptionColumnLayout, } from '../../constants' import { markdownParser } from '../../logic/ipynb' import { useCurrentWindowDimensions, useBoundingClientRect } from '../../hooks/graphics' @@ -18,8 +18,8 @@ import { useCurrentWindowDimensions, useBoundingClientRect } from '../../hooks/g const ArticleScrollamaSticky = ({ cell, currentStep, - marginTop=100, - marginBottom=50, + marginTop = 100, + marginBottom = 50, // marginRight=100, // marginLeft=50 }) => { @@ -36,34 +36,51 @@ const ArticleScrollamaSticky = ({ }, []) console.info('ArticleScrollamaSticky currentStep:', currentStep) return ( -
- +
+ - + - -
- {!cell.outputs.length ? ( -
-
- ): null} - {cell.outputs.map((output,i) => ( - + +
+ {!cell.outputs.length ?
: null} + {cell.outputs.map((output, i) => ( + ))}
-

'), - }} /> + +

'), + }} + /> + diff --git a/src/components/Article/ArticleToCStep.js b/src/components/Article/ArticleToCStep.js index 4f24c3c1..bda76284 100644 --- a/src/components/Article/ArticleToCStep.js +++ b/src/components/Article/ArticleToCStep.js @@ -1,36 +1,40 @@ import React from 'react' import { useHistory } from 'react-router-dom' -import { Layers, Image, Grid } from 'react-feather' +import { Layers, Grid } from 'react-feather' import { useArticleStore } from '../../store' - +import { MediaImage } from 'iconoir-react' const ArticleToCStep = ({ - idx, active=false, - level='', - isFigure=false, - isTable=false, - isHermeneutics=false, - isAccordionOpen=false, - isSectionStart=false, - isSectionEnd=false, + idx, + active = false, + level = '', + isFigure = false, + isTable = false, + isHermeneutics = false, + isAccordionOpen = false, + isSectionStart = false, + isSectionEnd = false, children, - width=0, - marginLeft=70, - className='' + width = 0, + marginLeft = 70, + className = '', }) => { const history = useHistory() - const setVisibleShadowCell = useArticleStore(state=>state.setVisibleShadowCell) - const displayLayer = useArticleStore(state=>state.displayLayer) + const setVisibleShadowCell = useArticleStore((state) => state.setVisibleShadowCell) + const displayLayer = useArticleStore((state) => state.displayLayer) const availableWidth = width - marginLeft const levelClassName = `ArticleToCStep_Level_${level}` - const labelClassName = isHermeneutics - ? 'ArticleToCStep_labelHermeneutics' - : !isHermeneutics && !isTable && !isFigure - ? 'ArticleToCStep_labelCircle' - : isFigure && !isTable - ? 'ArticleToCStep_labelFigure' - : 'ArticleToCStep_labelTable' + let labelClassName = '' + if (isHermeneutics) { + labelClassName = 'ArticleToCStep_labelHermeneutics' + } else if (!isHermeneutics && !isTable && !isFigure) { + labelClassName = 'ArticleToCStep_labelCircle' + } else if (isFigure && !isTable) { + labelClassName = 'ArticleToCStep_labelFigure' + } else { + labelClassName = 'ArticleToCStep_labelTable' + } const handleClick = () => { // if the layer is hidden, opens it up and scroll to it on click. @@ -44,17 +48,21 @@ const ArticleToCStep = ({ } } return ( -

- +
+
{isHermeneutics && !isTable && !isFigure && } {!isHermeneutics && !isTable && !isFigure &&
} - {isFigure && !isTable && } - {isTable && } + {isFigure && !isTable && } + {isTable && }
) diff --git a/src/components/ArticleV2/ArticleCellPlaceholder.css b/src/components/ArticleV2/ArticleCellPlaceholder.css new file mode 100644 index 00000000..9c5f22ab --- /dev/null +++ b/src/components/ArticleV2/ArticleCellPlaceholder.css @@ -0,0 +1,42 @@ +.ArticleCellPlaceholder__figure { + white-space: nowrap; + position: absolute; + right: 0; + margin-top: 0.5rem; +} + +.ArticleCellPlaceholder.dialog table td > div { + border-radius: 5px; + padding: 5px 10px; + display: inline-block; + background-color: rgba(255, 255, 255, 0.5); + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; +} + +.ArticleCellPlaceholder.dialog table { + border-collapse: separate; + border-spacing: 5px; +} + +.ArticleCellPlaceholder table { + font-size: 0.8em; +} + +.ArticleCellPlaceholder table td { + padding: var(--spacer-2); +} + +.ArticleCellPlaceholder.dialog table td { + padding: 5px; +} +.ArticleCellPlaceholder table th { + padding: 0 var(--spacer-2); +} + +.ArticleCellPlaceholder.dialog table td:last-child { + text-align: right; +} +.ArticleCellPlaceholder.dialog table td.empty { + box-shadow: none; + background: transparent; +} diff --git a/src/components/ArticleV2/ArticleCellPlaceholder.js b/src/components/ArticleV2/ArticleCellPlaceholder.js index 04a15378..1cc6a86a 100644 --- a/src/components/ArticleV2/ArticleCellPlaceholder.js +++ b/src/components/ArticleV2/ArticleCellPlaceholder.js @@ -3,61 +3,82 @@ import { Container, Row, Col } from 'react-bootstrap' import { BootstrapColumLayout } from '../../constants' import ArticleCellContent from '../Article/ArticleCellContent' import ArticleCellSourceCode from '../Article/ArticleCellSourceCode' -import {ArrowDown} from 'react-feather' +import { ArrowDown } from 'react-feather' +import { useTranslation } from 'react-i18next' +import './ArticleCellPlaceholder.css' +const ArticleCellPlaceholderParagraphNumbers = ({ nums = [], figure = null }) => { + const { t } = useTranslation() + + return ( +
+ {nums.length === 1 ? ( + nums[0] + ) : ( + <> + {nums[0]} +
+ +
+ {nums[nums.length - 1]} + + )} + {figure ? ( +
+ {t(figure.tNLabel, { n: figure.tNum })} +
+ ) : null} +
+ ) +} const ArticleCellPlaceholder = ({ - type='code', + type = 'code', layer, // whenever the placeholder stands for more than one paragraphs - nums=[], - content='', + nums = [], + content = '', idx, - headingLevel=0, - // isFigure=false - onNumClick + headingLevel = 0, + figure = null, + onNumClick, }) => { - const paragraphNumbers = nums.length - ? nums.length === 1 - ? nums[0] - : ( - - {nums[0]} -
- -
- {nums[nums.length -1]} -
- ) - : null + const paragraphNumbers = ArticleCellPlaceholderParagraphNumbers({ nums, figure }) + const onNumClickHandler = (e) => { - onNumClick(e, {layer, idx}) + onNumClick(e, { layer, idx }) } return ( - - {type === 'markdown' - ? ( - - ) - : ( - - ) - } + + {/* {figure ? ( + + ) : null} */} + {type === 'markdown' ? ( + + ) : ( + + )} diff --git a/src/components/ArticleV2/ArticleFlow.js b/src/components/ArticleV2/ArticleFlow.js index ea4d4991..8a76ee73 100644 --- a/src/components/ArticleV2/ArticleFlow.js +++ b/src/components/ArticleV2/ArticleFlow.js @@ -46,15 +46,13 @@ const ArticleFlow = ({ if (cell.layer === LayerHidden) { return } - if ( - i > 0 && - (cell.layer !== previousLayer || cell.isHeading || cell.isFigure || cell.isTable) - ) { + if (i > 0 && (cell.layer !== previousLayer || cell.isHeading || cell.figure !== null)) { buffers.push([...buffer]) buffer = [] } buffer.push(i) - // copy value + // update previous layer. If there is a figure and you want to isolate it, add figure suffix + // previousLayer = String(cell.layer) + (cell.figure !== null ? 'figure' : '') previousLayer = String(cell.layer) }) if (buffer.length) { diff --git a/src/components/ArticleV2/ArticleLayer.js b/src/components/ArticleV2/ArticleLayer.js index 58dab764..1ac05cd5 100644 --- a/src/components/ArticleV2/ArticleLayer.js +++ b/src/components/ArticleV2/ArticleLayer.js @@ -375,6 +375,7 @@ const ArticleLayer = ({ memoid={['pl', memoid, i, j].join('-')} {...paragraphs[j]} headingLevel={paragraphs[j].isHeading ? paragraphs[j].heading.level : 0} + figure={paragraphs[j].figure} nums={ firstCellInGroup.idx !== paragraphs[j].idx ? [] diff --git a/src/components/ArticleV2/ArticleToC.js b/src/components/ArticleV2/ArticleToC.js index cd11d952..b00498ff 100644 --- a/src/components/ArticleV2/ArticleToC.js +++ b/src/components/ArticleV2/ArticleToC.js @@ -104,6 +104,7 @@ const ArticleToC = ({ : cell.isFigure ? t(cell.figure.tNLabel, { n: cell.figure.tNum }) : '(na)', + figureRefPrefix: cell.isFigure ? cell.figure.refPrefix : null, isFigure: cell.isFigure, isTable: cell.isTable, isHermeneutics: cell.layer === LayerHermeneutics, diff --git a/src/components/ArticleV2/ArticleToCStep.js b/src/components/ArticleV2/ArticleToCStep.js index 819e83e3..4b5172d9 100644 --- a/src/components/ArticleV2/ArticleToCStep.js +++ b/src/components/ArticleV2/ArticleToCStep.js @@ -1,29 +1,33 @@ import React from 'react' -import { Layers, Image, Grid } from 'react-feather' +import { Layers, Grid } from 'react-feather' import { useArticleStore } from '../../store' - +import { MediaImage } from 'iconoir-react' const ArticleToCStep = ({ - cell, active=false, - isSectionStart=false, - isSectionEnd=false, + cell, + active = false, + isSectionStart = false, + isSectionEnd = false, children, - width=0, - marginLeft=70, - className='', + width = 0, + marginLeft = 70, + className = '', onStepClick, }) => { - const displayLayer = useArticleStore(state=>state.displayLayer) + const displayLayer = useArticleStore((state) => state.displayLayer) const availableWidth = width - marginLeft const levelClassName = `ArticleToCStep_Level_${cell.level}` - const labelClassName = cell.isHermeneutics - ? 'ArticleToCStep_labelHermeneutics' - : !cell.isHermeneutics && !cell.isTable && !cell.isFigure - ? 'ArticleToCStep_labelCircle' - : cell.isFigure && !cell.isTable - ? 'ArticleToCStep_labelFigure' - : 'ArticleToCStep_labelTable' + let labelClassName = '' + if (cell.isHermeneutics) { + labelClassName = 'ArticleToCStep_labelHermeneutics' + } else if (!cell.isHermeneutics && !cell.isTable && !cell.isFigure) { + labelClassName = 'ArticleToCStep_labelCircle' + } else if (cell.isFigure && !cell.isTable) { + labelClassName = 'ArticleToCStep_labelFigure' + } else { + labelClassName = 'ArticleToCStep_labelTable' + } const handleClick = () => { if (typeof onStepClick === 'function') { @@ -31,16 +35,24 @@ const ArticleToCStep = ({ } } return ( -
+
{cell.isHermeneutics && !cell.isTable && !cell.isFigure && } - {!cell.isHermeneutics && !cell.isTable && !cell.isFigure &&
} - {cell.isFigure && !cell.isTable && } + {!cell.isHermeneutics && !cell.isTable && !cell.isFigure && ( +
+ )} + {cell.isFigure && !cell.isTable ? : null} {cell.isTable && }
diff --git a/src/components/ToCStep.js b/src/components/ToCStep.js index 358a86f3..06ef499f 100644 --- a/src/components/ToCStep.js +++ b/src/components/ToCStep.js @@ -1,7 +1,23 @@ import React from 'react' -import { Layers, Image, Grid } from 'react-feather' +import { Layers } from 'react-feather' import { useArticleStore } from '../store' import '../styles/components/ToCStep.scss' +import { + DialogRefPrefix, + FigureRefPrefix, + TableRefPrefix, + VideoRefPrefix, + SoundRefPrefix, +} from '../constants' +import { MediaImage, MediaVideo, MessageText, SoundMin, Table } from 'iconoir-react' + +const FigureRefPrefixMapping = { + [FigureRefPrefix]: MediaImage, + [DialogRefPrefix]: MessageText, + [TableRefPrefix]: Table, + [SoundRefPrefix]: SoundMin, + [VideoRefPrefix]: MediaVideo, +} const ToCStep = ({ id = -1, @@ -11,6 +27,7 @@ const ToCStep = ({ isHermeneutics = false, isSectionStart = false, isSectionEnd = false, + figureRefPrefix = null, level = 'CODE', label = '', width = 100, @@ -23,19 +40,25 @@ const ToCStep = ({ const availableWidth = width - marginEnd const levelClassName = `ToCStep_Level_${level}` - const labelClassName = isHermeneutics - ? 'ToCStep_labelHermeneutics' - : !isHermeneutics && !isTable && !isFigure - ? 'ToCStep_labelCircle' - : isFigure && !isTable - ? 'ToCStep_labelFigure' - : 'ToCStep_labelTable' + let labelClassName = '' + if (isHermeneutics) { + labelClassName = 'ToCStep_labelHermeneutics' + } else if (!isHermeneutics && !isTable && !isFigure) { + labelClassName = 'ToCStep_labelCircle' + } else if (isFigure && !isTable) { + labelClassName = 'ToCStep_labelFigure' + } else { + labelClassName = 'ToCStep_labelTable' + } const handleClick = (e) => { if (typeof onClick === 'function') { onClick(e, { id, label }) } } + + const Icon = isFigure ? FigureRefPrefixMapping[figureRefPrefix] || MediaImage : null + return (
- {isHermeneutics && !isTable && !isFigure && } - {!isHermeneutics && !isTable && !isFigure &&
} - {isFigure && !isTable && } - {isTable && } + {isHermeneutics && !isFigure && } + {!isHermeneutics && !isFigure &&
} + {isFigure && }
) diff --git a/src/constants.js b/src/constants.js index 2182a0f2..7cd4c896 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,8 +1,8 @@ import { isMobile, isTablet } from 'react-device-detect' export const IsPortrait = window.innerWidth < window.innerHeight -export const IsMobile = isMobile ? true : false -export const IsTablet = isTablet ? true : false +export const IsMobile = Boolean(isMobile) +export const IsTablet = Boolean(isTablet) export const HomeRoute = { to: '/', label: 'navigation.home' } export const ReferencesRoute = { @@ -199,7 +199,23 @@ export const FigureRefPrefix = 'figure-' export const CoverRefPrefix = 'cover' export const TableRefPrefix = 'table-' export const QuoteRefPrefix = 'quote-' +export const DialogRefPrefix = 'dialog-' +export const SoundRefPrefix = 'sound-' +export const VideoRefPrefix = 'video-' export const AnchorRefPrefix = 'anchor-' +export const GalleryRefPrefix = 'gallery-' +export const AvailableFigureRefPrefixes = [ + FigureRefPrefix, + TableRefPrefix, + QuoteRefPrefix, + DialogRefPrefix, + SoundRefPrefix, + VideoRefPrefix, + GalleryRefPrefix, + CoverRefPrefix, +] +export const AvailableRefPrefixes = AvailableFigureRefPrefixes.concat([AnchorRefPrefix]) + // display Layer to enable switch between layers export const DisplayLayerHermeneutics = 'h' export const DisplayLayerNarrative = 'n' diff --git a/src/logic/ipynb.js b/src/logic/ipynb.js index 351978aa..9604e766 100644 --- a/src/logic/ipynb.js +++ b/src/logic/ipynb.js @@ -6,8 +6,6 @@ import ArticleHeading from '../models/ArticleHeading' import ArticleCell from '../models/ArticleCell' // import ArticleCellGroup from '../models/ArticleCellGroup' import ArticleReference from '../models/ArticleReference' -import ArticleFigure from '../models/ArticleFigure' -import ArticleAnchor from '../models/ArticleAnchor' import { SectionDefault, RoleHidden, @@ -15,14 +13,12 @@ import { RoleMetadata, RoleCitation, RoleDefault, - RoleQuote, CellTypeMarkdown, CellTypeCode, - FigureRefPrefix, - TableRefPrefix, - CoverRefPrefix, - QuoteRefPrefix, AnchorRefPrefix, + AvailableRefPrefixes, + AvailableFigureRefPrefixes, + DialogRefPrefix, } from '../constants' import ArticleTreeWarning, { FigureAnchorWarningCode, @@ -30,7 +26,12 @@ import ArticleTreeWarning, { ReferenceWarningCode, } from '../models/ArticleTreeWarning' -import { getSectionFromCellMetadata, getLayerFromCellMetadata } from './utils' +import { + getSectionFromCellMetadata, + getLayerFromCellMetadata, + getFigureFromCell, + getAnchorFromCell, +} from './utils' const encodeNotebookURL = (url) => btoa(encodeURIComponent(url)) const decodeNotebookURL = (encodedUrl) => decodeURIComponent(atob(encodedUrl)) @@ -42,12 +43,20 @@ const renderMarkdownWithReferences = ({ citationsFromMetadata = {}, figures = [], anchors = [], + figure = null, // ArticleFigure instance if any, null otherwise }) => { const references = [] const warnings = [] + const prefixRegex = new RegExp( + '([^<]+)', + 'ig', + ) // console.info('markdownParser.render', markdownParser.render(sources)) - const content = markdownParser + let content = markdownParser .render(sources) + // enable
+ .replace(/<br\/>/g, '
') + .replace(/<br>/g, '
') // add target blank for all external links .replace(/
{ if (href.indexOf('http') === 0) { @@ -57,19 +66,16 @@ const renderMarkdownWithReferences = ({ }) .replace(/<a[^&]*>(.*)<\/a>/g, '') // replace links "figure-*" ending with automatic numbering syntax - .replace( - /([^<]+)<\/a>/g, - (m, anchorRef, c, content) => { - const ref = - anchorRef.indexOf('anchor-') !== -1 - ? anchors.find((d) => d.ref === anchorRef) - : figures.find((d) => d.ref === anchorRef) - if (ref) { - return `figure ${ref.num}` - } - return `${content}` - }, - ) + .replace(prefixRegex, (m, anchorRef, c, content) => { + const ref = + anchorRef.indexOf('anchor-') !== -1 + ? anchors.find((d) => d.ref === anchorRef) + : figures.find((d) => d.ref === anchorRef) + if (ref) { + return `figure ${ref.num}` + } + return `${content}` + }) // replace links "figure-" add data-idx attribute containing a figure id .replace(//g, (m, anchorRef) => { const ref = @@ -138,6 +144,15 @@ const renderMarkdownWithReferences = ({ ` }) + // add specific replacement for specific figure RefPrefix + if (figure?.refPrefix === DialogRefPrefix) { + // eslint-disable-next-line + content = content + .replace(/\s*<\/td>/g, '') + .replace(/\s* \s*<\/td>/g, '') + .replace(/(.*)<\/td>/g, '
$1
') + } + return { content, references, warnings } } @@ -150,9 +165,17 @@ const getArticleTreeFromIpynb = ({ id, cells = [], metadata = {} }) => { const anchors = [] const sectionsIndex = {} const warnings = [] + // this contain footnotes => zotero id to remap reference at paragraph level + const referenceIndex = {} + let bibliography = null let citationsFromMetadata = metadata?.cite2c?.citations - let tableAutonumbering = 0 - + // initialize figure numbering using constants/AvailableFigureRefPrefixes + const figureNumberingByRefPrefix = AvailableFigureRefPrefixes.reduce((acc, prefix) => { + acc[prefix] = 0 + return acc + }, {}) + let paragraphNumber = 0 + // parse citations if (citationsFromMetadata) { // if one of the key is named "udefined" (sic) if (citationsFromMetadata.undefined) { @@ -178,36 +201,40 @@ const getArticleTreeFromIpynb = ({ id, cells = [], metadata = {} }) => { return acc }, {}) } - // this contain footnotes => zotero id to remap reference at paragraph level - const referenceIndex = {} - // - let bibliography = null + // parse biobliographic elements if (citationsFromMetadata instanceof Object) { bibliography = new Cite(Object.values(citationsFromMetadata).filter((d) => d)) } - let paragraphNumber = 0 + // cycle through notebook cells to fill ArticleCells, figures, headings cells .map((cell, idx) => { - const sources = Array.isArray(cell.source) ? cell.source.join('\n') : cell.source - // find footnote citations (with the number) - const footnote = sources.match(//) - const coverRef = cell.metadata.tags?.find((d) => d.indexOf(CoverRefPrefix) === 0) - const figureRef = cell.metadata.tags?.find((d) => d.indexOf(FigureRefPrefix) === 0) - const tableRef = cell.metadata.tags?.find((d) => d.indexOf(TableRefPrefix) === 0) - const quoteRef = cell.metadata.tags?.find((d) => d.indexOf(QuoteRefPrefix) === 0) - const anchorRef = cell.metadata.tags?.find((d) => d.indexOf(AnchorRefPrefix) === 0) - // get section and layer from metadata + cell.idx = idx + // get section and layer from cell metadata cell.section = getSectionFromCellMetadata(cell.metadata) cell.layer = getLayerFromCellMetadata(cell.metadata) cell.role = RoleDefault - // get role in a secont step - if ( - sources.length === 0 || - cell.metadata.tags?.includes('hidden') || - cell.metadata.jdh?.hidden - ) { + + const sources = Array.isArray(cell.source) ? cell.source.join('\n') : cell.source + // find footnote citations (with the number) + const footnote = sources.match(//) + const figure = getFigureFromCell(cell, AvailableFigureRefPrefixes) + const anchor = getAnchorFromCell(cell, [AnchorRefPrefix]) + const isHidden = + sources.length === 0 || cell.metadata.tags?.includes('hidden') || cell.metadata.jdh?.hidden + + if (figure) { + // update global index of figure numbering for this specific prefix and forward it to the figure + figureNumberingByRefPrefix[figure.refPrefix] += 1 + figure.setNum(+figureNumberingByRefPrefix[figure.refPrefix]) + figures.push(figure) + cell.figure = figure + cell.role = RoleFigure + } else if (anchor) { + cell.anchor = anchor + anchors.push(anchor) + } else if (isHidden) { // is hidden (e.g. uninteresting code, like pip install) cell.hidden = true cell.role = RoleHidden @@ -216,70 +243,10 @@ const getArticleTreeFromIpynb = ({ id, cells = [], metadata = {} }) => { referenceIndex[footnote[1]] = footnote[2] cell.hidden = true cell.role = RoleCitation - } else if (figureRef) { - // this is a proper figure, nothing to say about it. - figures.push( - new ArticleFigure({ - ref: figureRef, - idx, - num: figures.length + 1, - }), - ) - cell.role = RoleFigure - } else if (coverRef) { - figures.push(new ArticleFigure({ ref: coverRef, idx, isCover: true })) - cell.role = RoleFigure - } else if (tableRef) { - tableAutonumbering += 1 - // this is a proper figure, nothing to say about it. - figures.push( - new ArticleFigure({ - ref: tableRef, - idx, - isTable: true, - num: +tableAutonumbering, - }), - ) - cell.role = RoleFigure - } else if (quoteRef) { - cell.role = RoleQuote - } else if ( - idx < cells.length && - cell.cell_type === 'code' && - Array.isArray(cell.outputs) && - cell.outputs.length - ) { - // this is a "Figure" candindate. - // Let's check whether the cell outputs JDH metadata and if jdh namespace contains **module**; - const cellOutputJdhMetadata = cell.outputs.find((d) => d.metadata?.jdh?.module) - if (cellOutputJdhMetadata) { - // if yes, these metadata AND its output will be added to this cell. - cell.metadata = { - ...cell.metadata, - jdh: { - ...cell.metadata.jdh, - ...cellOutputJdhMetadata.metadata.jdh, - ref: idx, - outputs: cell.outputs, - }, - } - figures.push( - new ArticleFigure({ - module: cell.metadata.jdh.module, - type: cell.metadata.jdh.object?.type, - idx, - num: figures.length + 1, - }), - ) - cell.role = RoleFigure - } - // this is not a real "Figure", just a "Data..." } else if (cell.section !== SectionDefault) { cell.role = RoleMetadata } - if (anchorRef) { - anchors.push(new ArticleAnchor({ ref: anchorRef, idx })) - } + cell.source = Array.isArray(cell.source) ? cell.source : [cell.source] if (cell.role !== RoleMetadata) { paragraphNumber += 1 @@ -316,6 +283,7 @@ const getArticleTreeFromIpynb = ({ id, cells = [], metadata = {} }) => { citationsFromMetadata, figures, anchors, + figure: cell.figure, }) if (postRenderWarnings.length) { warnings.push(...postRenderWarnings) @@ -372,7 +340,8 @@ const getArticleTreeFromIpynb = ({ id, cells = [], metadata = {} }) => { hidden: !!cell.hidden, heading: headerIdx > -1 ? headings[headings.length - 1] : null, level: headerIdx > -1 ? tokens[headerIdx].tag : 'p', - figure: cell.role === RoleFigure ? figures.find((d) => d.idx === idx) : null, + figure: cell.figure, + anchor: cell.anchor, }), ) } else if (cell.cell_type === CellTypeCode) { diff --git a/src/logic/utils.js b/src/logic/utils.js index fc886007..a4d56754 100644 --- a/src/logic/utils.js +++ b/src/logic/utils.js @@ -1,4 +1,6 @@ import { LayerChoices, LayerNarrative, SectionChoices, SectionDefault } from '../constants' +import ArticleAnchor from '../models/ArticleAnchor' +import ArticleFigure from '../models/ArticleFigure' import ArticleReference from '../models/ArticleReference' import ArticleTreeWarning, { FigureAnchorWarningCode, @@ -61,6 +63,65 @@ export const getLayerFromCellMetadata = (metadata) => LayerNarrative, ) +/** + * getFigureFromCell + * + * Returns an ArticleFigure object based on the given cell's metadata tags. + * @param {Object} cell - The cell object to extract the figure from. + * @returns {ArticleFigure|null} - The ArticleFigure object if found, otherwise null. + */ +/** + * Returns an ArticleFigure object from a cell's metadata tags that match the given refPrefixes. + * @param {Object} cell - The cell object to extract the figure from. + * @param {Array} [refPrefixes=[]] - An array of reference prefixes to match against the cell's metadata tags. + * @returns {ArticleFigure|null} - An ArticleFigure object if a matching tag is found, otherwise null. + */ +export const getFigureFromCell = (cell, refPrefixes = []) => { + if (!cell.metadata?.tags) { + return null + } + let figure = null + for (const refPrefix of refPrefixes) { + for (const tag of cell.metadata.tags) { + if (tag.indexOf(refPrefix) === 0) { + figure = new ArticleFigure({ + ref: tag, + idx: cell.idx, + refPrefix, + }) + break + } + } + } + return figure +} + +/** + * Returns an ArticleAnchor object from a cell's metadata tags that match the given refPrefixes. + * @param {Object} cell - The cell object to extract the anchor from. + * @param {Array} refPrefixes - An array of reference prefixes to match against the cell's metadata tags. + * @returns {ArticleAnchor|null} - An ArticleAnchor object if a matching tag is found, otherwise null. + */ +export const getAnchorFromCell = (cell, refPrefixes = []) => { + if (!cell.metadata?.tags) { + return null + } + let anchor = null + for (const refPrefix of refPrefixes) { + for (const tag of cell.metadata.tags) { + if (tag.indexOf(refPrefix) === 0) { + anchor = new ArticleAnchor({ + ref: tag, + idx: cell.idx, + refPrefix, + }) + break + } + } + } + return anchor +} + export const renderMarkdownWithReferences = ({ idx = -1, sources = '', diff --git a/src/models/ArticleFigure.js b/src/models/ArticleFigure.js index fe12b378..d35b1084 100644 --- a/src/models/ArticleFigure.js +++ b/src/models/ArticleFigure.js @@ -1,3 +1,5 @@ +import { CoverRefPrefix, FigureRefPrefix } from '../constants' + export default class ArticleFigure { constructor({ ref = null, // 'figure-', // 'figure-12a' figure identifier. Must start with constants/FigureRefPrefix @@ -6,7 +8,7 @@ export default class ArticleFigure { idx = -1, num = -1, isTable = false, - isCover = false + refPrefix = FigureRefPrefix, }) { this.type = type this.module = module @@ -14,12 +16,29 @@ export default class ArticleFigure { this.num = num this.ref = ref this.isTable = isTable - this.isCover = isCover - this.tNLabel = this.isTable ? 'numbers.table': 'numbers.figure' - this.tNum = typeof this.ref === 'string' - ? this.ref.lastIndexOf('-*') !== -1 - ? this.num - : this.ref.split('-').pop() - : this.num + this.isCover = refPrefix === CoverRefPrefix + this.tNLabel = this.isCover + ? 'cover' + : `numbers.${refPrefix.substring(0, refPrefix.length - 1)}` + // number in the table of contents. It is more important than `num` because it is used to sort the figures + this.refPrefix = refPrefix + } + + getPrefix() { + return this.refPrefix.substring(0, this.refPrefix.length - 1) + } + + setNum(num) { + this.num = num + if (typeof this.ref !== 'string' || this.ref.lastIndexOf('-*') !== -1) { + this.tNum = this.num + return + } + const refNum = this.ref.split('-').pop() + if (isNaN(refNum)) { + this.tNum = this.num + } else { + this.tNum = parseInt(refNum) + } } } diff --git a/src/stories/ArticleCellWithDialogue.stories.js b/src/stories/ArticleCellWithDialogue.stories.js new file mode 100644 index 00000000..156dc599 --- /dev/null +++ b/src/stories/ArticleCellWithDialogue.stories.js @@ -0,0 +1,68 @@ +import React from 'react' +import { useIpynbNotebookParagraphs } from '../hooks/ipynb' +import ArticleCell from '../components/Article/ArticleCell' + +export default { + title: 'ArticleCell with dialogue', + component: ArticleCell, + argTypes: { + metadata: { control: { type: 'object' }, defaultValue: {} }, + }, +} + +const CellWithDialogue = { + cell_type: 'markdown', + metadata: { + tags: ['table-border-xp-*', 'dialog-border-xp-*'], + }, + source: [ + 'Termine | Aura\n', + '--- | ---\n', + 'so eahm to me the border it has a very familiar like \n', + 'it’s very familiar to me because I grew up on the border with Slovenia |  \n', + '  |  \n', + 'and in fact \n', + 'I am part of the Slovenian minority in Italy |  \n', + '  | OK\n', + 'so since I was born \n', + 'I was little \n', + 'I always \n', + 'with my family we always travelled from Italy to Slovenia \n', + 'like on the regular basis daily \n', + 'so to me the border wasn’t at the beginning \n', + 'when when there was still like \n', + 'the euhm physical border with the (unclear) that goes up and down \n', + 'there was like \n', + 'I wouldn’t call it a shock \n', + 'because I grew up with it so I got to know it \n', + 'but like when euhm like the the police stop you at the border and said to you \n', + 'give me your ID give me your prekusnica \n', + 'which was a type of passport the people who lived on the border had \n', + 'that was quite like euhm it wasn’t normal for me \n', + 'and then when the border was taken down \n', + 'and now we can you can pass it whatever you like it \n', + 'or how many times you like it \n', + 'now it’s different but eahm \n', + 'so as I said |  ', + ], +} + +const Template = ({ cells, metadata, isJavascriptTrusted }) => { + const articleTree = useIpynbNotebookParagraphs({ + id: 'memoid', + cells, + metadata, + }) + return [ + articleTree.paragraphs.map((p, i) => ( + + )), + ] +} +export const Default = Template.bind({}) + +Default.args = { + isJavascriptTrusted: true, + metadata: {}, + cells: [CellWithDialogue], +} diff --git a/src/styles/article.scss b/src/styles/article.scss index 2d0e8a51..4c30230c 100644 --- a/src/styles/article.scss +++ b/src/styles/article.scss @@ -279,10 +279,13 @@ $breakpoint-lg: 992px; tbody tr:nth-of-type(odd) { background-color: var(--gray-200); } - td, - th { + + td { padding: var(--spacer-2); } + th { + padding: 0 var(--spacer-2); + } } .ArticleCellFigure img { @@ -398,21 +401,6 @@ ArticleShadowLayer_animatedLabel { height: 20px; } -.ArticleFigure { - min-height: 50px; - margin-top: var(--spacer-3); -} - -.ArticleFigure_figcaption_num { - position: absolute; - left: -140px; - width: 140px; - text-align: right; - padding-right: var(--spacer-3); - font-family: var(--font-family-monospace); - font-weight: bold; -} - svg.ArticleFingerprint { g.type-code { // is type===cell diff --git a/src/styles/components/Article/ArticleCellFigure.scss b/src/styles/components/Article/ArticleCellFigure.scss index 91b37bbc..7894196a 100644 --- a/src/styles/components/Article/ArticleCellFigure.scss +++ b/src/styles/components/Article/ArticleCellFigure.scss @@ -23,3 +23,41 @@ top: 0; } } +.ArticleCellFigure.figure { + display: block; +} +.ArticleCellFigure.dialog { + table { + background: transparent; + border: none; + border-collapse: separate; + border-spacing: 5px; + td > div { + border-radius: 5px; + padding: 5px 10px; + display: inline-block; + background-color: rgba(255, 255, 255, 0.5); + // simple box shadow + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; + } + td.empty { + box-shadow: none; + background: transparent; + } + // last td in each row + td:last-child { + text-align: right; + + div { + text-align: left; + } + } + } + table tbody tr:nth-of-type(2n + 1) { + background-color: transparent; + } + + table td { + padding: 5px; + } +} diff --git a/src/translations.json b/src/translations.json index de9710da..e3b9be00 100644 --- a/src/translations.json +++ b/src/translations.json @@ -245,7 +245,9 @@ }, "numbers": { "issue": "Issue n.{{n}}", + "dialog": "dialog {{ n }}", "figure": "figure {{ n }}", + "sound": "sound {{ n }}", "table": "table {{ n }}", "yExponent": "Power scale exponent (y axis): {{n}}", "errors": "{{count}} errors", diff --git a/yarn.lock b/yarn.lock index ee5c36dc..3856b821 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9493,6 +9493,11 @@ i18next@^19.8.4: dependencies: "@babel/runtime" "^7.12.0" +iconoir-react@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/iconoir-react/-/iconoir-react-6.11.0.tgz#a88d896148c8389138ec931ce7367f3abc6a7144" + integrity sha512-+1RgmEWh/9H0aYR2e8sDL5elDUYnkyOXO0E+RnwhkjhJY36Lge6r/9nv1n1FvDXoeFY48omkPUl8s/ciA0XOAg== + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"