<commit_before>import { TemplateExecutor } from 'lodash'
import template from 'lodash.template'
import { DATA_BUTTON_ID, CLASS_HOST } from './constants'
import { RawFootnote } from '.'
import { TemplateData, Settings } from '../types'

const CLASS_PRINT_ONLY = 'footnote-print-only'
const CLASS_PROCESSED = 'footnote-processed'

type RefBody = readonly [HTMLElement, HTMLElement]
type RefBodyData = readonly [HTMLElement, HTMLElement, TemplateData]

const setPrintOnly = (el: Element) => el.classList.add(CLASS_PRINT_ONLY)

function children(element: Element, selector?: string): Element[] {
  return Array.from(element.children).filter(
    child => child.nodeType !== 8 && (!selector || child.matches(selector))
  )
}

function isDefined<T>(value?: T): value is T {
  return value !== undefined
}

function getLastFootnoteId(): string {
  const footnotes = document.querySelectorAll(`[${DATA_BUTTON_ID}]`)
  const lastFootnoteId =
    footnotes.length &&
    footnotes[footnotes.length - 1].getAttribute(DATA_BUTTON_ID)
  return lastFootnoteId || '0'
}

function findFootnoteLinks({
  anchorPattern,
  anchorParentSelector,
  footnoteParentClass,
  scope = ''
}: Settings): readonly HTMLAnchorElement[] {
  return Array.from(
    document.querySelectorAll<HTMLAnchorElement>(scope + ' a[href^="#"]')
  ).filter(
    link =>
      (link.href + link.rel).match(anchorPattern) &&
      !link.closest(
        `.${footnoteParentClass}:not(a):not(${anchorParentSelector})`
      )
  )
}

const findFootnoteRefBody = ({
  allowDuplicates,
  anchorParentSelector,
  footnoteSelector
}: Settings) => (link: HTMLAnchorElement): RefBody | undefined => {
  const [_, fragment] = link.href.split('#')
  const selector = '#' + fragment.replace(/[:.+~*[\]]/g, '\\$&')
  const related = document.querySelector(
    allowDuplicates ? selector : `${selector}:not(.${CLASS_PROCESSED})`
  )
  const parent = link.closest(anchorParentSelector) as HTMLElement
  const body = related && (related.closest(footnoteSelector) as HTMLElement)

  if (body) {
    body.classList.add(CLASS_PROCESSED)
    return [parent || link, body]
  }
}

function prepareContent(content: string, reference: string): string {
  const pattern = reference.trim().replace(/\s+/g, '|')
  const regex = new RegExp(
    '(\\s|&nbsp;)*<\\s*a[^#<]*#(' + pattern + ')[^>]*>(.*?)<\\s*/\\s*a>',
    'g'
  )

  let preparedContent = content
    .trim()
    .replace(regex, '')
    .replace('[]', '')

  if (preparedContent.indexOf('<') !== 0) {
    preparedContent = '<p>' + preparedContent + '</p>'
  }

  return preparedContent
}

const resetNumbers = (resetSelector: string) => (
  [ref, body, data]: RefBodyData,
  n: number,
  footnotes: RefBodyData[]
): RefBodyData => {
  const previousNumber = n ? footnotes[n - 1][2].number : 0
  return [
    ref,
    body,
    {
      ...data,
      number: ref.closest(resetSelector) ? 1 : previousNumber + 1
    }
  ]
}

const templateData = (offset: number) => (
  [ref, body]: RefBody,
  idx: number
): RefBodyData => {
  const reference = ref.id
  const footnoteNumber = offset + idx
  return [
    ref,
    body,
    {
      reference,
      content: prepareContent(body.innerHTML, reference),
      id: `${footnoteNumber}`,
      number: footnoteNumber
    }
  ]
}

function hideFootnoteContainer(container: HTMLElement): void {
  const visibleElements = children(container, `:not(.${CLASS_PRINT_ONLY})`)
  const visibleSeparators = visibleElements.filter(el => el.tagName === 'HR')

  if (visibleElements.length === visibleSeparators.length) {
    visibleSeparators.concat(container).forEach(setPrintOnly)
    hideFootnoteContainer(container.parentNode as HTMLElement)
  }
}

const addButton = (render: TemplateExecutor) => ([
  reference,
  body,
  data
]: RefBodyData): RawFootnote => {
  reference.insertAdjacentHTML(
    'beforebegin',
    `<span class="${CLASS_HOST}">${render(data)}</span>`
  )
  const host = reference.previousElementSibling!
  const button = host.firstElementChild as HTMLElement
  return { data, reference, body, button, maxHeight: 0 }
}

function hideOriginalFootnote([reference, body]: RefBody): RefBody {
  setPrintOnly(reference)
  setPrintOnly(body)
  hideFootnoteContainer(body.parentNode as HTMLElement)
  return [reference, body]
}

export function createDocumentFootnotes(settings: Settings): RawFootnote[] {
  const { anchorParentSelector, buttonTemplate, numberResetSelector } = settings
  const offset = parseInt(getLastFootnoteId(), 10) + 1

  return findFootnoteLinks(settings)
    .map(findFootnoteRefBody(settings))
    .filter(isDefined)
    .map(hideOriginalFootnote)
    .map(templateData(offset))
    .map(numberResetSelector ? resetNumbers(numberResetSelector) : i => i)
    .map(addButton(template(buttonTemplate)))
}<commit_msg>Refactor<commit_after>import { TemplateExecutor } from 'lodash'
import template from 'lodash.template'
import { DATA_BUTTON_ID, CLASS_HOST } from './constants'
import { RawFootnote } from '.'
import { TemplateData, Settings } from '../types'

const CLASS_PRINT_ONLY = 'footnote-print-only'
const CLASS_PROCESSED = 'footnote-processed'

type RefBody = readonly [HTMLElement, HTMLElement]
type RefBodyData = readonly [HTMLElement, HTMLElement, TemplateData]

const setPrintOnly = (el: Element) => el.classList.add(CLASS_PRINT_ONLY)

function children(element: Element, selector?: string): Element[] {
  return Array.from(element.children).filter(
    child => child.nodeType !== 8 && (!selector || child.matches(selector))
  )
}

function isDefined<T>(value?: T): value is T {
  return value !== undefined
}

function getLastFootnoteId(): string {
  const footnotes = document.querySelectorAll(`[${DATA_BUTTON_ID}]`)
  const lastFootnoteId =
    footnotes.length &&
    footnotes[footnotes.length - 1].getAttribute(DATA_BUTTON_ID)
  return lastFootnoteId || '0'
}

function findFootnoteLinks({
  anchorPattern,
  anchorParentSelector,
  footnoteParentClass,
  scope = ''
}: Settings): readonly HTMLAnchorElement[] {
  return Array.from(
    document.querySelectorAll<HTMLAnchorElement>(scope + ' a[href^="#"]')
  ).filter(
    link =>
      (link.href + link.rel).match(anchorPattern) &&
      !link.closest(
        `.${footnoteParentClass}:not(a):not(${anchorParentSelector})`
      )
  )
}

const findRefBody = ({
  allowDuplicates,
  anchorParentSelector,
  footnoteSelector
}: Settings) => (link: HTMLAnchorElement): RefBody | undefined => {
  const [_, fragment] = link.href.split('#')
  const selector = '#' + fragment.replace(/[:.+~*[\]]/g, '\\$&')
  const related = document.querySelector(
    allowDuplicates ? selector : `${selector}:not(.${CLASS_PROCESSED})`
  )
  const body = related && (related.closest(footnoteSelector) as HTMLElement)

  if (body) {
    body.classList.add(CLASS_PROCESSED)
    const reference = link.closest(anchorParentSelector) as HTMLElement
    return [reference || link, body]
  }
}

function prepareContent(content: string, reference: string): string {
  const pattern = reference.trim().replace(/\s+/g, '|')
  const regex = new RegExp(
    '(\\s|&nbsp;)*<\\s*a[^#<]*#(' + pattern + ')[^>]*>(.*?)<\\s*/\\s*a>',
    'g'
  )

  const preparedContent = content
    .trim()
    .replace(regex, '')
    .replace('[]', '')

  return preparedContent.startsWith('<')
    ? preparedContent
    : '<p>' + preparedContent + '</p>'
}

const resetNumbers = (resetSelector: string) => (
  [ref, body, data]: RefBodyData,
  n: number,
  footnotes: RefBodyData[]
): RefBodyData => {
  const previousNumber = n ? footnotes[n - 1][2].number : 0
  return [
    ref,
    body,
    { ...data, number: ref.closest(resetSelector) ? 1 : previousNumber + 1 }
  ]
}

const templateData = (offset: number) => (
  [reference, body]: RefBody,
  idx: number
): RefBodyData => {
  const data: TemplateData = {
    reference: reference.id,
    content: prepareContent(body.innerHTML, reference.id),
    id: `${offset + idx}`,
    number: offset + idx
  }

  return [reference, body, data]
}

function hideFootnoteContainer(container: HTMLElement): void {
  const visibleElements = children(container, `:not(.${CLASS_PRINT_ONLY})`)
  const visibleSeparators = visibleElements.filter(el => el.tagName === 'HR')

  if (visibleElements.length === visibleSeparators.length) {
    visibleSeparators.concat(container).forEach(setPrintOnly)
    hideFootnoteContainer(container.parentNode as HTMLElement)
  }
}

const addButton = (render: TemplateExecutor) => ([
  reference,
  body,
  data
]: RefBodyData): RawFootnote => {
  reference.insertAdjacentHTML(
    'beforebegin',
    `<span class="${CLASS_HOST}">${render(data)}</span>`
  )
  const host = reference.previousElementSibling!
  const button = host.firstElementChild as HTMLElement
  return { data, reference, body, button, maxHeight: 0 }
}

function hideOriginalFootnote([reference, body]: RefBody): RefBody {
  setPrintOnly(reference)
  setPrintOnly(body)
  hideFootnoteContainer(body.parentNode as HTMLElement)
  return [reference, body]
}

export function createDocumentFootnotes(settings: Settings): RawFootnote[] {
  const { buttonTemplate, numberResetSelector } = settings
  const offset = parseInt(getLastFootnoteId(), 10) + 1

  return findFootnoteLinks(settings)
    .map(findRefBody(settings))
    .filter(isDefined)
    .map(hideOriginalFootnote)
    .map(templateData(offset))
    .map(numberResetSelector ? resetNumbers(numberResetSelector) : i => i)
    .map(addButton(template(buttonTemplate)))
}
{'fraction_non_alphanumeric': 0.14656359207878852, 'fraction_numerical': 0.0026474637297469023, 'mean_word_length': 4.452655889145497, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6397332', 'n_tokens_mistral': 2957, 'n_tokens_neox': 2909, 'n_words': 622}