Aller au contenu

Colophon⚓︎

Ce site est construit avec Material for MkDocs et le fichier de configuration est le suivant :

mkdocs.yml
site_name: Code.larlet.fr
site_url: 'https://code.larlet.fr'
site_author: David Larlet
site_description: >-
  Partage de petits bouts de codes utiles et commentés en Python, JS, CSS, etc.
copyright: Site par <a href="https://larlet.fr/david/">David Larlet</a>.
repo_url: https://gitlab.com/davidbgk/larlet-fr-code
edit_uri: edit/main/docs/
site_dir: public
watch:
  - docs
  - code
  - mkdocs.yml
theme:
  language: fr
  font: false  # (1)
  name: material
  custom_dir: overrides
  features:
    - content.code.annotate
    - navigation.top
    - toc.integrate
  icon:
    repo: fontawesome/brands/git
  palette:
    # Palette toggle for light mode
    - media: "(prefers-color-scheme: light)"
      scheme: default
      primary: teal
      accent: blue
      toggle:
        icon: material/weather-night
        name: Switch to dark mode

    # Palette toggle for dark mode
    - media: "(prefers-color-scheme: dark)"
      scheme: slate
      primary: teal
      accent: blue
      toggle:
        icon: material/weather-sunny
        name: Switch to light mode

nav:
  - 'index.md'
  - 'Python':
    - 'Généralités': 'python.md'
    - 'Serveurs': 'python-servers.md'
  - 'JavaScript':
    - 'Généralités': 'javascript.md'
    - 'Formulaires': 'javascript-forms.md'
    - 'Images': 'javascript-images.md'
    - 'Fetch': 'javascript-fetch.md'
    - 'Web Components': 'javascript-webcomponents.md'
  - 'css.md'
  - 'html.md'
  - 'makefile.md'
  - 'shell.md'
  - 'regexp.md'
  - 'sql.md'
  - 'pages.md'
  - 'colophon.md'

extra_javascript:
  - static/javascript/termynal.js
  - static/javascript/termynal-setup.js
extra_css:
  - static/stylesheets/termynal.css
  - static/stylesheets/fonts.css
  - static/stylesheets/extra.css
plugins:
  - tags
  - search
  - git-revision-date-localized:
      type: iso_date
      fallback_to_build_date: true
  # Requires a repository setup.
  - rss:
      abstract_chars_count: -1
      date_from_meta:
        as_creation: "date"
        as_update: false
        datetime_format: "%Y-%m-%d %H:%M"
      length: 10
      pretty_print: false
extra:
  generator: false
  social:
    - icon: fontawesome/brands/mastodon
      link: https://mastodon.social/@dav
    - icon: material/rss-box
      link: /feed_rss_created.xml

markdown_extensions:
  - toc:
      permalink: ⚓︎
  - abbr
  - attr_list
  - md_in_html
  - admonition
  - pymdownx.details
  - pymdownx.inlinehilite
  - pymdownx.superfences
  - pymdownx.keys
  - pymdownx.mark
  - pymdownx.snippets:
      auto_append:
        - includes/abbreviations.md
  - pymdownx.highlight:
      anchor_linenums: true
  - pymdownx.emoji:
      emoji_index: !!python/name:materialx.emoji.twemoji
      emoji_generator: !!python/name:materialx.emoji.to_svg
  1. 🙅‍♂️ désactivation des polices Google

Il utilise les polices FiraCode et GolosUI définies ainsi :

fonts.css
@font-face {
  font-family: 'golos_ui';
  src: url('../fonts/Golos-UI_Regular.woff2') format('woff2'),
       url('../fonts/Golos-UI_Regular.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'golos_ui';
  src: url('../fonts/Golos-UI_Bold.woff2') format('woff2'),
       url('../fonts/Golos-UI_Bold.woff') format('woff');
  font-weight: bold;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'firacode_retina';
  src: url('../fonts/FiraCode-Retina.woff2') format('woff2'),
       url('../fonts/FiraCode-Retina.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

Les terminaux qui s’animent sont faits à base de Termynal, en s’inspirant de la documentation de FastAPI, l’initialisation est faite ainsi :

termynal-setup.js
function setupTermynal() {
  document.querySelectorAll('.use-termynal').forEach((node) => {
    node.style.display = 'block'
    new Termynal(node, {
      lineDelay: 500,
    })
  })
  const progressLiteralStart = '---> 100%'
  const promptLiteralStart = '$ '
  const customPromptLiteralStart = '# '
  const termynalActivateClass = 'termy'
  let termynals = []

  function createTermynals() {
    document
      .querySelectorAll(`.${termynalActivateClass} .highlight`)
      .forEach((node) => {
        const text = node.textContent
        const lines = text.split('\n')
        const useLines = []
        let buffer = []
        function saveBuffer() {
          if (buffer.length) {
            let isBlankSpace = true
            buffer.forEach((line) => {
              if (line) {
                isBlankSpace = false
              }
            })
            dataValue = {}
            if (isBlankSpace) {
              dataValue['delay'] = 0
            }
            if (buffer[buffer.length - 1] === '') {
              // A last single <br> won't have effect
              // so put an additional one
              buffer.push('')
            }
            const bufferValue = buffer.join('<br>')
            dataValue['value'] = bufferValue
            useLines.push(dataValue)
            buffer = []
          }
        }
        for (let line of lines) {
          if (line === progressLiteralStart) {
            saveBuffer()
            useLines.push({
              type: 'progress',
            })
          } else if (line.startsWith(promptLiteralStart)) {
            saveBuffer()
            // Ensure we keep important spaces
            // (for intro art text for instance).
            const value = line
              .replace(promptLiteralStart, '')
              .replaceAll(' ', '&nbsp;')
            useLines.push({
              type: 'input',
              value: value,
            })
          } else if (line.startsWith('// ')) {
            saveBuffer()
            const value = '💬 ' + line.replace('// ', '').trimEnd()
            useLines.push({
              value: value,
              class: 'termynal-comment',
              delay: 0,
            })
          } else if (line.startsWith(customPromptLiteralStart)) {
            saveBuffer()
            const promptStart = line.indexOf(promptLiteralStart)
            if (promptStart === -1) {
              console.error('Custom prompt found but no end delimiter', line)
            }
            const prompt = line
              .slice(0, promptStart)
              .replace(customPromptLiteralStart, '')
            let value = line.slice(promptStart + promptLiteralStart.length)
            useLines.push({
              type: 'input',
              value: value,
              prompt: prompt,
            })
          } else {
            // Ensure we keep important spaces
            // (for Makefiles’ help for instance).
            buffer.push(line.replaceAll(' ', '&nbsp;'))
          }
        }
        saveBuffer()
        const div = document.createElement('div')
        node.replaceWith(div)
        const termynal = new Termynal(div, {
          lineData: useLines,
          noInit: true,
          lineDelay: 500,
        })
        termynals.push(termynal)
      })
  }

  function loadVisibleTermynals() {
    termynals = termynals.filter((termynal) => {
      if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) {
        termynal.init()
        return false
      }
      return true
    })
  }
  window.addEventListener('scroll', loadVisibleTermynals)
  createTermynals()
  loadVisibleTermynals()
}

setupTermynal()

Une adaptation a été tirée de FastAPI pour avoir des boutons de passage rapide et recommencement :

termynal.js
/**
 * termynal.js
 * A lightweight, modern and extensible animated terminal window, using
 * async/await.
 *
 * @author Sebastián Ramírez, Ines Montani <ines@ines.io>
 * @version https://github.com/tiangolo/fastapi/blob/
 *          5c576e42be8f8832f0e225beaa72153a256892d7/docs/en/docs/js/termynal.js
 * @license MIT
 */

'use strict'

/** Generate a terminal widget. */
class Termynal {
  /**
   * Construct the widget's settings.
   * @param {(string|Node)=} container - Query selector or container element.
   * @param {Object=} options - Custom settings.
   * @param {string} options.prefix - Prefix to use for data attributes.
   * @param {number} options.startDelay - Delay before animation, in ms.
   * @param {number} options.typeDelay - Delay between each typed character, in ms.
   * @param {number} options.lineDelay - Delay between each line, in ms.
   * @param {number} options.progressLength - Number of characters displayed as progress bar.
   * @param {string} options.progressChar – Character to use for progress bar, defaults to █.
   * @param {number} options.progressPercent - Max percent of progress.
   * @param {string} options.cursor – Character to use for cursor, defaults to ▋.
   * @param {Object[]} lineData - Dynamically loaded line data objects.
   * @param {boolean} options.noInit - Don't initialise the animation.
   */
  constructor(container = '#termynal', options = {}) {
    this.container =
      typeof container === 'string'
        ? document.querySelector(container)
        : container
    this.pfx = `data-${options.prefix || 'ty'}`
    this.originalStartDelay = this.startDelay =
      options.startDelay ||
      parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) ||
      600
    this.originalTypeDelay = this.typeDelay =
      options.typeDelay ||
      parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) ||
      90
    this.originalLineDelay = this.lineDelay =
      options.lineDelay ||
      parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) ||
      1500
    this.progressLength =
      options.progressLength ||
      parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) ||
      40
    this.progressChar =
      options.progressChar ||
      this.container.getAttribute(`${this.pfx}-progressChar`) ||
      '█'
    this.progressPercent =
      options.progressPercent ||
      parseFloat(
        this.container.getAttribute(`${this.pfx}-progressPercent`)
      ) ||
      100
    this.cursor =
      options.cursor ||
      this.container.getAttribute(`${this.pfx}-cursor`) ||
      '▋'
    this.lineData = this.lineDataToElements(options.lineData || [])
    this.loadLines()
    if (!options.noInit) this.init()
  }

  loadLines() {
    // Load all the lines and create the container so that the size is fixed
    // Otherwise it would be changing and the user viewport would be constantly
    // moving as she/he scrolls
    const finish = this.generateFinish()
    finish.style.visibility = 'hidden'
    this.container.appendChild(finish)
    // Appends dynamically loaded lines to existing line elements.
    this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(
      this.lineData
    )
    for (let line of this.lines) {
      line.style.visibility = 'hidden'
      this.container.appendChild(line)
    }
    const restart = this.generateRestart()
    restart.style.visibility = 'hidden'
    this.container.appendChild(restart)
    this.container.setAttribute('data-termynal', '')
  }

  /**
   * Initialise the widget, get lines, clear container and start animation.
   */
  init() {
    /**
     * Calculates width and height of Termynal container.
     * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS.
     */
    const containerStyle = getComputedStyle(this.container)
    this.container.style.width =
      containerStyle.width !== '0px' ? containerStyle.width : undefined
    this.container.style.minHeight =
      containerStyle.height !== '0px' ? containerStyle.height : undefined

    this.container.setAttribute('data-termynal', '')
    this.container.innerHTML = ''
    for (let line of this.lines) {
      line.style.visibility = 'visible'
    }
    this.start()
  }

  /**
   * Start the animation and rener the lines depending on their data attributes.
   */
  async start() {
    this.addFinish()
    await this._wait(this.startDelay)

    for (let line of this.lines) {
      const type = line.getAttribute(this.pfx)
      const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay

      if (type == 'input') {
        line.setAttribute(`${this.pfx}-cursor`, this.cursor)
        await this.type(line)
        await this._wait(delay)
      } else if (type == 'progress') {
        await this.progress(line)
        await this._wait(delay)
      } else {
        this.container.appendChild(line)
        await this._wait(delay)
      }

      line.removeAttribute(`${this.pfx}-cursor`)
    }
    this.addRestart()
    this.finishElement.style.visibility = 'hidden'
    this.lineDelay = this.originalLineDelay
    this.typeDelay = this.originalTypeDelay
    this.startDelay = this.originalStartDelay
  }

  generateRestart() {
    const restart = document.createElement('a')
    restart.onclick = (e) => {
      e.preventDefault()
      this.container.innerHTML = ''
      this.init()
    }
    restart.href = '#'
    restart.setAttribute('data-terminal-control', '')
    restart.innerHTML = 'restart ↻'
    return restart
  }

  generateFinish() {
    const finish = document.createElement('a')
    finish.onclick = (e) => {
      e.preventDefault()
      this.lineDelay = 0
      this.typeDelay = 0
      this.startDelay = 0
    }
    finish.href = '#'
    finish.setAttribute('data-terminal-control', '')
    finish.innerHTML = 'fast →'
    this.finishElement = finish
    return finish
  }

  addRestart() {
    const restart = this.generateRestart()
    this.container.appendChild(restart)
  }

  addFinish() {
    const finish = this.generateFinish()
    this.container.appendChild(finish)
  }

  /**
   * Animate a typed line.
   * @param {Node} line - The line element to render.
   */
  async type(line) {
    const chars = [...line.textContent]
    line.textContent = ''
    this.container.appendChild(line)

    for (let char of chars) {
      const delay =
        line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay
      await this._wait(delay)
      line.textContent += char
    }
  }

  /**
   * Animate a progress bar.
   * @param {Node} line - The line element to render.
   */
  async progress(line) {
    const progressLength =
      line.getAttribute(`${this.pfx}-progressLength`) || this.progressLength
    const progressChar =
      line.getAttribute(`${this.pfx}-progressChar`) || this.progressChar
    const chars = progressChar.repeat(progressLength)
    const progressPercent =
      line.getAttribute(`${this.pfx}-progressPercent`) || this.progressPercent
    line.textContent = ''
    this.container.appendChild(line)

    for (let i = 1; i < chars.length + 1; i++) {
      await this._wait(this.typeDelay)
      const percent = Math.round((i / chars.length) * 100)
      line.textContent = `${chars.slice(0, i)} ${percent}%`
      if (percent > progressPercent) {
        break
      }
    }
  }

  /**
   * Helper function for animation delays, called with `await`.
   * @param {number} time - Timeout, in ms.
   */
  _wait(time) {
    return new Promise((resolve) => setTimeout(resolve, time))
  }

  /**
   * Converts line data objects into line elements.
   *
   * @param {Object[]} lineData - Dynamically loaded lines.
   * @param {Object} line - Line data object.
   * @returns {Element[]} - Array of line elements.
   */
  lineDataToElements(lineData) {
    return lineData.map((line) => {
      let div = document.createElement('div')
      div.innerHTML = `<span ${this._attributes(line)}>${
        line.value || ''
      }</span>`

      return div.firstElementChild
    })
  }

  /**
   * Helper function for generating attributes string.
   *
   * @param {Object} line - Line data object.
   * @returns {string} - String of attributes.
   */
  _attributes(line) {
    let attrs = ''
    for (let prop in line) {
      // Custom add class
      if (prop === 'class') {
        attrs += ` class=${line[prop]} `
        continue
      }
      if (prop === 'type') {
        attrs += `${this.pfx}="${line[prop]}" `
      } else if (prop !== 'value') {
        attrs += `${this.pfx}-${prop}="${line[prop]}" `
      }
    }

    return attrs
  }
}

/**
 * HTML API: If current script has container(s) specified, initialise Termynal.
 */
if (document.currentScript.hasAttribute('data-termynal-container')) {
  const containers = document.currentScript.getAttribute(
    'data-termynal-container'
  )
  containers.split('|').forEach((container) => new Termynal(container))
}

TODO

Appliquer mes propres conseils et ne pas lancer l’animation en fonction des paramètres utilisateur·ice !


Dernière mise à jour: 2022-08-23