ElectronicBabylonianLiterature/ebl-frontend

View on GitHub
src/transliteration/ui/DisplayToken.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
A
97%
import React, { FunctionComponent, PropsWithChildren } from 'react'
import classNames from 'classnames'
import _ from 'lodash'
import {
  effectiveEnclosure,
  EgyptianMetricalFeetSeparator,
  EnclosureType,
  Gloss,
  GreekLetter,
  NamedSign,
  Sign,
  Token,
  UnknownSign,
  Variant,
  Word,
} from 'transliteration/domain/token'
import { addAccents } from 'transliteration/domain/accents'
import {
  isEnclosure,
  isBreak,
  isAkkadianWord,
} from 'transliteration/domain/type-guards'
import { createModifierClasses, Modifiers } from './modifiers'
import EnclosureFlags from './EnclosureFlags'
import Flags from './Flags'
import SubIndex from 'transliteration/ui/Subindex'
import WordInfoWithPopover, { WordInfo } from './WordInfo'
import { useLineGroupContext } from './LineGroupContext'
import { LineGroup } from './LineGroup'
import AkkadianWordComponent from 'akkadian/ui/akkadianWord'
import { PhoneticProps } from 'akkadian/application/phonetics/segments'

export type TokenWrapper = FunctionComponent<PropsWithChildren<unknown>>

export interface TokenProps {
  token: Token
  Wrapper: TokenWrapper
  tokenClasses?: readonly string[]
  lineGroup?: LineGroup
  isInPopover?: boolean
  showMeter?: boolean
  showIpa?: boolean
  phoneticProps?: PhoneticProps
}

export function DamagedFlag({
  sign: { flags },
  Wrapper,
  children,
}: PropsWithChildren<{
  sign: { flags: readonly string[] }
  Wrapper: TokenWrapper
}>): JSX.Element {
  return flags.includes('#') ? (
    <>
      <Wrapper>
        <span className="Transliteration__bracket">⸢</span>
      </Wrapper>
      {children}
      <Wrapper>
        <span className="Transliteration__bracket">⸣</span>
      </Wrapper>
    </>
  ) : (
    <>{children}</>
  )
}

function DefaultToken({ token, Wrapper }: TokenProps): JSX.Element {
  return (
    <EnclosureFlags token={token}>
      {token.parts ? (
        token.parts.map((token, index) => (
          <DisplayToken key={index} token={token} Wrapper={Wrapper} />
        ))
      ) : isEnclosure(token) ? (
        token.value
      ) : (
        <Wrapper>{token.value}</Wrapper>
      )}
    </EnclosureFlags>
  )
}

function VariantComponent({ token, Wrapper }: TokenProps): JSX.Element {
  return (
    <>
      {(token as Variant).tokens.map((token, index) => (
        <React.Fragment key={index}>
          {index > 0 ? <Wrapper>/</Wrapper> : null}
          <DisplayToken token={token} Wrapper={Wrapper} />
        </React.Fragment>
      ))}
    </>
  )
}

function GlossComponent({ token, Wrapper }: TokenProps): JSX.Element {
  const gloss = token as Gloss
  const GlossWrapper: TokenWrapper = ({
    children,
  }: PropsWithChildren<unknown>) => (
    <Wrapper>
      <sup>{children}</sup>
    </Wrapper>
  )
  return (
    <>
      <span
        className={classNames([
          'Transliteration__glossJoiner',
          ...createModifierClasses('glossJoiner', token.enclosureType),
        ])}
      >
        <GlossWrapper>.</GlossWrapper>
      </span>
      {gloss.parts.map((token, index) =>
        isEnclosure(token) ? (
          <DisplayToken key={index} token={token} />
        ) : (
          <DisplayToken key={index} token={token} Wrapper={GlossWrapper} />
        )
      )}
    </>
  )
}

function UnknownSignComponent({ token, Wrapper }: TokenProps): JSX.Element {
  const sign = token as UnknownSign
  const signs = {
    UnclearSign: sign.enclosureType.includes('BROKEN_AWAY') ? 'o' : 'x',
    UnidentifiedSign: 'X',
  }
  return (
    <DamagedFlag sign={sign} Wrapper={Wrapper}>
      <Wrapper>
        <EnclosureFlags token={sign}>
          {signs[sign.type]}
          <Flags flags={sign.flags} />
        </EnclosureFlags>
      </Wrapper>
    </DamagedFlag>
  )
}

function EgyptianMetricalFeetSeparatorComponent({
  token,
  Wrapper,
}: TokenProps): JSX.Element {
  const sign = token as EgyptianMetricalFeetSeparator
  return (
    <DamagedFlag sign={sign} Wrapper={Wrapper}>
      <Wrapper>
        <EnclosureFlags token={token}>
          <span className="Transliteration__EgyptianMetricalFeetSeparator--colored">
            {'•'}
          </span>
          <Flags flags={sign.flags} />
        </EnclosureFlags>
      </Wrapper>
    </DamagedFlag>
  )
}

function signComponent(nameProperty: string) {
  return function SignComponent({ token, Wrapper }: TokenProps): JSX.Element {
    const sign = token as Sign
    return (
      <DamagedFlag sign={sign} Wrapper={Wrapper}>
        <Wrapper>
          <EnclosureFlags token={token}>
            {sign[nameProperty]}
            <Modifiers modifiers={sign.modifiers} />
            <Flags flags={sign.flags} />
          </EnclosureFlags>
        </Wrapper>
      </DamagedFlag>
    )
  }
}

function NamedSignComponent({ token, Wrapper }: TokenProps): JSX.Element {
  const namedSign = token as NamedSign
  const effectiveEnclosures: EnclosureType[] = effectiveEnclosure(namedSign)
  const [parts, isSubIndexConverted] = addAccents(namedSign)
  const omitSubindex = namedSign.subIndex === 1 || isSubIndexConverted
  return (
    <DamagedFlag sign={namedSign} Wrapper={Wrapper}>
      <EnclosureFlags token={namedSign} enclosures={effectiveEnclosures}>
        {parts.map((token, index) =>
          isEnclosure(token) ? (
            <DisplayToken key={index} token={token} />
          ) : (
            <DisplayToken key={index} token={token} Wrapper={Wrapper} />
          )
        )}
        <Wrapper>
          {!omitSubindex && <SubIndex token={namedSign} />}
          <Modifiers modifiers={namedSign.modifiers} />
          <Flags flags={namedSign.flags} />
        </Wrapper>
        {namedSign.sign && (
          <>
            <span className="Transliteration__bracket">(</span>
            <DisplayToken token={namedSign.sign} Wrapper={Wrapper} />
            <span className="Transliteration__bracket">)</span>
          </>
        )}
        {namedSign.surrogate && !_.isEmpty(namedSign.surrogate) && (
          <>
            <span className="Transliteration__bracket">&lt;(</span>
            {namedSign.surrogate.map((token, index) => (
              <DisplayToken key={index} token={token} Wrapper={Wrapper} />
            ))}
            <span className="Transliteration__bracket">)&gt;</span>
          </>
        )}
      </EnclosureFlags>
    </DamagedFlag>
  )
}

function GreekLetterComponent({ token, Wrapper }: TokenProps): JSX.Element {
  const letter = token as GreekLetter
  return (
    <DamagedFlag sign={letter} Wrapper={Wrapper}>
      <Wrapper>
        <EnclosureFlags token={letter}>
          {letter.letter}
          <Flags flags={letter.flags} />
        </EnclosureFlags>
      </Wrapper>
    </DamagedFlag>
  )
}

function TabulationComponent({ token, Wrapper }: TokenProps): JSX.Element {
  return (
    <Wrapper>
      <span></span>
    </Wrapper>
  )
}

function LineBreakComponent({ Wrapper }: TokenProps): JSX.Element {
  return <Wrapper>|</Wrapper>
}

function WordComponent({
  token,
  Wrapper,
  tokenClasses: modifierClasses,
  lineGroup,
  isInPopover,
  phoneticProps,
}: TokenProps): JSX.Element {
  const word = token as Word
  const WordInfoComponent = isInPopover ? WordInfo : WordInfoWithPopover
  return (
    <WordInfoComponent
      word={word}
      tokenClasses={modifierClasses ?? []}
      lineGroup={lineGroup}
    >
      <EnclosureFlags token={token}>
        {word.parts.map((token, index) => (
          <DisplayToken
            key={index}
            token={token}
            Wrapper={Wrapper}
            phoneticProps={phoneticProps}
          />
        ))}
      </EnclosureFlags>
    </WordInfoComponent>
  )
}

const tokens: ReadonlyMap<
  Token['type'],
  FunctionComponent<{
    token: Token
    Wrapper: TokenWrapper
  }>
> = new Map([
  ['UnknownNumberOfSigns', (): JSX.Element => <>…</>],
  ['Variant', VariantComponent],
  ['EgyptianMetricalFeetSeparator', EgyptianMetricalFeetSeparatorComponent],
  ['Reading', NamedSignComponent],
  ['Logogram', NamedSignComponent],
  ['Number', NamedSignComponent],
  ['Divider', signComponent('divider')],
  ['Grapheme', signComponent('name')],
  ['UnclearSign', UnknownSignComponent],
  ['UnidentifiedSign', UnknownSignComponent],
  ['Determinative', GlossComponent],
  ['PhoneticGloss', GlossComponent],
  ['LinguisticGloss', GlossComponent],
  ['Tabulation', TabulationComponent],
  ['LineBreak', LineBreakComponent],
  ['GreekLetter', GreekLetterComponent],
  ['AkkadianWord', AkkadianWordComponent],
  ['Word', WordComponent],
])

interface DisplayTokenProps {
  token: Token
  bemModifiers?: readonly string[]
  Wrapper?: FunctionComponent<PropsWithChildren<unknown>>
  lineGroup?: LineGroup
  isInPopover?: boolean
  showMeter?: boolean
  showIpa?: boolean
  phoneticProps?: PhoneticProps
}

export default function DisplayToken({
  token,
  bemModifiers = [],
  Wrapper = ({ children }: PropsWithChildren<unknown>): JSX.Element => (
    <>{children}</>
  ),
  lineGroup,
  isInPopover = false,
  showMeter = false,
  showIpa = false,
  phoneticProps = {},
}: DisplayTokenProps): JSX.Element {
  const TokenComponent = tokens.get(token.type) ?? DefaultToken
  const tokenClasses = [
    `Transliteration__${token.type}`,
    ...createModifierClasses(token.type, bemModifiers),
  ]

  if (token.alignment && lineGroup?.highlightIndex === token.alignment) {
    tokenClasses.push('Transliteration__inAlignSet')
  }

  return (
    <span
      className={classNames(tokenClasses, {
        hidden: isBreak(token) && !showMeter,
      })}
    >
      <TokenComponent
        token={token}
        Wrapper={Wrapper}
        tokenClasses={tokenClasses}
        lineGroup={lineGroup}
        isInPopover={isInPopover}
        showMeter={showMeter}
        showIpa={showIpa}
        {...(isAkkadianWord(token) && { phoneticProps })}
      />
    </span>
  )
}

export function DisplayLineGroupToken({
  token,
  bemModifiers = [],
  Wrapper = ({ children }: PropsWithChildren<unknown>): JSX.Element => (
    <>{children}</>
  ),
  showMeter = false,
  showIpa = false,
  phoneticProps = {},
}: DisplayTokenProps): JSX.Element {
  const lineGroup = useLineGroupContext()

  return (
    <DisplayToken
      token={token}
      bemModifiers={bemModifiers}
      Wrapper={Wrapper}
      lineGroup={lineGroup}
      showMeter={showMeter}
      showIpa={showIpa}
      phoneticProps={phoneticProps}
    />
  )
}