Console Importer

Easily import JS and CSS resources from Chrome console.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @author       Lete114
// @version      0.1
// @license      MIT License
// @name         Console Importer
// @description  Easily import JS and CSS resources from Chrome console.
// @match        *://*/*
// @icon         data:image/png;base64,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
// @namespace    https://github.com/Lete114/my-tampermonkey-scripts
// @grant        unsafeWindow
// ==/UserScript==

;(function () {
  'use strict'
  // https://github.com/pd4d10/console-importer
  const window = unsafeWindow
  const tiza = new Tiza()
  const PREFIX_TEXT = '[$i]: '
  const prefix = tiza.color('blue').text
  const strong = tiza.color('blue').bold().text
  const error = tiza.color('red').text
  const log = (...args) => tiza.log(prefix(PREFIX_TEXT), ...args)
  const logError = (...args) => tiza.log(error(PREFIX_TEXT), ...args)

  /**
   * @type { Set<string> | null }
   */
  let lastGlobalVariableSet = null

  /**
   *
   * @param { string } name
   * @returns
   */
  function createBeforeLoad(name) {
    return () => {
      lastGlobalVariableSet = new Set(Object.keys(window))
      log(strong(name), ' is loading, please be patient...')
    }
  }

  /**
   *
   * @param { string } name
   * @param { string? } url
   * @returns
   */
  function createOnLoad(name, url) {
    return () => {
      const urlText = url ? `(${url})` : ''
      log(strong(name), `${urlText} is loaded.`)

      const currentGlobalVariables = Object.keys(window)
      const newGlobalVariables = currentGlobalVariables.filter(
        (key) => !lastGlobalVariableSet?.has(key)
      )
      if (newGlobalVariables.length > 0) {
        log(
          'The new global variables are as follows: ',
          strong(newGlobalVariables.join(',')),
          ' . Maybe you can use them.'
        )
      } else {
        // maybe css request or script loaded already
      }
      // Update global variable list
      lastGlobalVariableSet = new Set(currentGlobalVariables)
    }
  }

  /**
   *
   * @param { string } name
   * @param { string? } url
   * @returns
   */
  function createOnError(name, url) {
    return () => {
      const urlText = url ? `(${strong(url)})` : ''
      logError(
        'Fail to load ',
        strong(name),
        ', is this URL',
        urlText,
        ' correct?'
      )
    }
  }

  // Try to remove referrer for security
  // https://imququ.com/post/referrer-policy.html
  // https://www.w3.org/TR/referrer-policy/
  function addNoReferrerMeta() {
    const originMeta =
      document.querySelector < HTMLMetaElement > 'meta[name=referrer]'

    if (originMeta) {
      // If there is already a referrer policy meta tag, save origin content
      // and then change it, call `remove` to restore it
      const content = originMeta.content
      originMeta.content = 'no-referrer'
      return function remove() {
        originMeta.content = content
      }
    } else {
      // Else, create a new one, call `remove` to delete it
      const meta = document.createElement('meta')
      meta.name = 'referrer'
      meta.content = 'no-referrer'
      document.head.appendChild(meta)
      return function remove() {
        // Removing meta tag directly not work, should set it to default first
        meta.content = 'no-referrer-when-downgrade'
        document.head.removeChild(meta)
      }
    }
  }

  /**
   * Insert script tag
   * @param { sting } url
   * @param { function } onload
   * @param { function } onerror
   */
  function injectScript(url, onload, onerror) {
    const remove = addNoReferrerMeta()
    const script = document.createElement('script')
    script.src = url
    script.onload = onload
    script.onerror = onerror
    document.body.appendChild(script)
    remove()
    document.body.removeChild(script)
  }

  /**
   * Insert link tag
   * @param { string } url
   * @param { function } onload
   * @param { function } onerror
   */
  function injectStyle(url, onload, onerror) {
    const remove = addNoReferrerMeta()
    const link = document.createElement('link')
    link.href = url
    link.rel = 'stylesheet'
    // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#Stylesheet_load_events
    link.onload = onload
    link.onerror = onerror
    document.head.appendChild(link)
    remove()
    // Should not remove <link> tag, unlike <script>
  }

  /**
   *
   * @param { string } url
   * @param {*} beforeLoad
   * @param {*} onload
   * @param {*} onerror
   * @returns
   */
  function inject(
    url,
    beforeLoad = createBeforeLoad(url),
    onload = createOnLoad(url),
    onerror = createOnError(url)
  ) {
    beforeLoad()

    // Handle CSS
    if (/\.css$/.test(url)) {
      return injectStyle(url, onload, onerror)
    }

    // Handle JS
    return injectScript(url, onload, onerror)
  }

  /**
   * From cdnjs
   * https://cdnjs.com/
   * @param { string } name
   */
  function cdnjs(name) {
    log('Searching for ', strong(name), ', please be patient...')
    fetch(`https://api.cdnjs.com/libraries?search=${name}`, {
      referrerPolicy: 'no-referrer'
    })
      .then((res) => res.json())
      .then(({ results }) => {
        if (results.length === 0) {
          logError(
            'Sorry, ',
            strong(name),
            ' not found, please try another keyword.'
          )
          return
        }

        const matchedResult = results.filter((item) => item.name === name)
        const { name: exactName, latest: url } = matchedResult[0] || results[0]
        if (name !== exactName) {
          log(
            strong(name),
            ' not found, import ',
            strong(exactName),
            ' instead.'
          )
        }

        inject(
          url,
          createBeforeLoad(exactName),
          createOnLoad(exactName, url),
          createOnError(exactName, url)
        )
      })
      .catch(() => {
        logError(
          'There appears to be some trouble with your network. If you think this is a bug, please report an issue:'
        )
        logError('https://github.com/pd4d10/console-importer/issues')
      })
  }

  /**
   * From unpkg
   * https://unpkg.com
   * @param { string } name
   */
  function unpkg(name) {
    createBeforeLoad(name)()
    const url = `https://unpkg.com/${name}`
    injectScript(url, createOnLoad(name, url), createOnError(name, url))
  }

  /**
   * https://www.jsdelivr.com/esm
   * @param { string } name
   * @returns
   */
  async function esm(name) {
    log(strong(name), '(esm) is loading, please be patient...')
    const res = await import(`https://esm.run/${name}`)
    return res
  }

  /**
   * Entry
   * @param { string } originName
   * @returns
   */
  function importer(originName) {
    if (typeof originName !== 'string') {
      throw new Error('Argument should be a string, please check it.')
    }

    // Trim string
    const name = originName.trim()

    // If it is a valid URL, inject it directly
    if (/^https?:\/\//.test(name)) {
      return inject(name)
    }

    // If version specified, try unpkg
    if (name.indexOf('@') !== -1) {
      return unpkg(name)
    }

    return cdnjs(name)
  }

  importer.cdnjs = cdnjs
  importer.unpkg = unpkg
  importer.esm = esm

  // Do not output annoying ugly string of function content
  importer.toString = () => '$i'

  // Assign to console
  console.$i = importer

  // Do not break existing $i
  const win = window
  if (typeof win.$i === 'undefined') {
    win.$i = importer
  } else {
    log('$i is already in use, please use `console.$i` instead')
  }

  function Tiza(currentStyles, texts, styles) {
    if (currentStyles === void 0) {
      currentStyles = []
    }
    if (texts === void 0) {
      texts = []
    }
    if (styles === void 0) {
      styles = []
    }
    var _this = this
    // Get method
    this.getCurrentStyles = function () {
      return _this._currentStyles
    }
    this.getTexts = function () {
      return _this._texts
    }
    this.getStyles = function () {
      return _this._styles
    }
    // Push a style to current Styles
    this.style = function (s) {
      return new Tiza(
        _this._currentStyles.concat([s]),
        _this._texts,
        _this._styles
      )
    }
    // Alias for style method
    this.color = function (c) {
      return _this.style('color:' + c)
    }
    this.bgColor = function (c) {
      return _this.style('background-color:' + c)
    }
    this.bold = function () {
      return _this.style('font-weight:bold')
    }
    this.italic = function () {
      return _this.style('font-style:italic')
    }
    this.size = function (n) {
      var s = typeof n === 'number' ? n + 'px' : n // Convert number to px
      return _this.style('font-size:' + s)
    }
    // Clear all current styles
    this.reset = function () {
      return new Tiza([], _this._texts, _this._styles)
    }
    this.text = function () {
      var args = []
      for (var _i = 0; _i < arguments.length; _i++) {
        args[_i] = arguments[_i]
      }
      var texts = _this._texts.slice()
      var styles = _this._styles.slice()
      args.forEach(function (arg) {
        if (arg instanceof Tiza) {
          texts.push.apply(texts, arg.getTexts())
          styles.push.apply(styles, arg.getStyles())
        } else {
          texts.push(arg)
          styles.push(_this._currentStyles.join(';'))
        }
      })
      return new Tiza(_this._currentStyles, texts, styles)
    }
    this.space = function (count) {
      if (count === void 0) {
        count = 1
      }
      return _this.text(repeat(' ', count))
    }
    this.newline = function (count) {
      if (count === void 0) {
        count = 1
      }
      return _this.text(repeat('\n', count))
    }
    this._output = function (type) {
      return function () {
        var args = []
        for (var _i = 0; _i < arguments.length; _i++) {
          args[_i] = arguments[_i]
        }
        var ins = _this.text.apply(_this, args)
        console[type].apply(
          console,
          [
            ins
              .getTexts()
              .map(function (t) {
                return '%c' + t
              })
              .join('')
          ].concat(ins._styles)
        )
        return new Tiza(ins.getCurrentStyles())
      }
    }
    this.log = this._output('log')
    this.info = this._output('info')
    this.warn = this._output('warn')
    this.error = this._output('error')
    this._currentStyles = currentStyles
    this._texts = texts
    this._styles = styles
  }
})()