zestedesavoir / zmarkdown

Live demo: https://zestedesavoir.github.io/zmarkdown/
MIT License
226 stars 53 forks source link

Support For Inline CSS Styles #410

Closed KaptianCore closed 4 years ago

KaptianCore commented 4 years ago

Like we can set classes would be good if we could do inline css

StaloneLab commented 4 years ago

Hello,

your issue lacks a lot of information... What plugin are you referring to? Why would you need inline CSS? Where do you think you can "set classes"? Could you please edit to add some minimal information; an example of the syntax you'd like or what you're trying to acheive would also be great.

KaptianCore commented 4 years ago

I want to do it as I want to make a block that allows me to do text colouring with inline css would look like this. [color='worded colours, hex colour or rgb colour']Red Text[/color]

    spoiler: {
      classes: 'spoiler-block',
      style: 'color: red'
      title: 'optional',
      details: true
    },

this is for the custom blocks plugin, I want to actually use it on the remark.js parser.

StaloneLab commented 4 years ago

I'm not against this feature, but likely won't do it myself anytime soon. Maybe you'd be interested in working on it? Should be fairly easy to do.

KaptianCore commented 4 years ago

Idk I'm not very confident with Remarks plugin system, I'll give it a crack maybe.

StaloneLab commented 4 years ago

Well, that would require modifying one of our plugin, so should be easier than creating your own. I'm able to help you if you have any problem.

KaptianCore commented 4 years ago

I just wanna know how to start lol

I mean I could take a guess and just like basically mimic what is done to add classes instead to add styles. this is what I have so far

const spaceSeparated = require('space-separated-tokens')

function escapeRegExp (str) {
  return str.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, 'g'), '\\$&')
}

const C_NEWLINE = '\n'
const C_FENCE = '|'

function compilerFactory (nodeType) {
  let text
  let title

  return {
    blockHeading (node) {
      title = this.all(node).join('')
      return ''
    },
    blockBody (node) {
      text = this.all(node).map(s => s.replace(/\n/g, '\n| ')).join('\n|\n| ')
      return text
    },
    block (node) {
      text = ''
      title = ''
      this.all(node)
      if (title) {
        return `[[${nodeType} | ${title}]]\n| ${text}`
      } else {
        return `[[${nodeType}]]\n| ${text}`
      }
    },
  }
}

module.exports = function blockPlugin (availableBlocks = {}) {
  const pattern = Object
    .keys(availableBlocks)
    .map(escapeRegExp)
    .join('|')

  if (!pattern) {
    throw new Error('remark-custom-blocks needs to be passed a configuration object as option')
  }

  const regex = new RegExp(`\\[\\[(${pattern})(?: *\\| *(.*))?\\]\\]\n`)

  function blockTokenizer (eat, value, silent) {
    const now = eat.now()
    const keep = regex.exec(value)
    if (!keep) return
    if (keep.index !== 0) return
    const [eaten, blockType, blockTitle] = keep

    /* istanbul ignore if - never used (yet) */
    if (silent) return true

    const linesToEat = []
    const content = []

    let idx = 0
    while ((idx = value.indexOf(C_NEWLINE)) !== -1) {
      const next = value.indexOf(C_NEWLINE, idx + 1)
      // either slice until next NEWLINE or slice until end of string
      const lineToEat = next !== -1 ? value.slice(idx + 1, next) : value.slice(idx + 1)
      if (lineToEat[0] !== C_FENCE) break
      // remove leading `FENCE ` or leading `FENCE`
      const line = lineToEat.slice(lineToEat.startsWith(`${C_FENCE} `) ? 2 : 1)
      linesToEat.push(lineToEat)
      content.push(line)
      value = value.slice(idx + 1)
    }

    const contentString = content.join(C_NEWLINE)

    const stringToEat = eaten + linesToEat.join(C_NEWLINE)

    const potentialBlock = availableBlocks[blockType]
    const titleAllowed = potentialBlock.title &&
      ['optional', 'required'].includes(potentialBlock.title)
    const titleRequired = potentialBlock.title && potentialBlock.title === 'required'

    if (titleRequired && !blockTitle) return
    if (!titleAllowed && blockTitle) return

    const add = eat(stringToEat)
    if (potentialBlock.details) {
      potentialBlock.containerElement = 'details'
      potentialBlock.titleElement = 'summary'
    }

    const exit = this.enterBlock()
    const contents = {
      type: `${blockType}CustomBlockBody`,
      data: {
        hName: potentialBlock.contentsElement ? potentialBlock.contentsElement : 'div',
        hProperties: {
          className: 'custom-block-body',
        },
      },
      children: this.tokenizeBlock(contentString, now),
    }
    exit()

    const blockChildren = [contents]
    if (titleAllowed && blockTitle) {

      const titleElement = potentialBlock.titleElement ? potentialBlock.titleElement : 'div'
      const titleNode = {
        type: `${blockType}CustomBlockHeading`,
        data: {
          hName: titleElement,
          hProperties: {
            className: 'custom-block-heading',
          },
        },
        children: this.tokenizeInline(blockTitle, now),
      }

      blockChildren.unshift(titleNode)
    }
    const styleList = spaceSeparated.parse(potentialBlock.styles || '')
    const classList = spaceSeparated.parse(potentialBlock.classes || '')

    return add({
      type: `${blockType}CustomBlock`,
      children: blockChildren,
      data: {
        hName: potentialBlock.containerElement ? potentialBlock.containerElement : 'div',
        hProperties: {
          className: ['custom-block', ...classList],
          styleName: ['custom-style', ...styleList],
        },
      },
    })
  }

  const Parser = this.Parser

  // Inject blockTokenizer
  const blockTokenizers = Parser.prototype.blockTokenizers
  const blockMethods = Parser.prototype.blockMethods
  blockTokenizers.customBlocks = blockTokenizer
  blockMethods.splice(blockMethods.indexOf('fencedCode') + 1, 0, 'customBlocks')
  const Compiler = this.Compiler
  if (Compiler) {
    const visitors = Compiler.prototype.visitors
    if (!visitors) return
    Object.keys(availableBlocks).forEach(key => {
      const compiler = compilerFactory(key)
      visitors[`${key}CustomBlock`] = compiler.block
      visitors[`${key}CustomBlockHeading`] = compiler.blockHeading
      visitors[`${key}CustomBlockBody`] = compiler.blockBody
    })
  }
  // Inject into interrupt rules
  const interruptParagraph = Parser.prototype.interruptParagraph
  const interruptList = Parser.prototype.interruptList
  const interruptBlockquote = Parser.prototype.interruptBlockquote
  interruptParagraph.splice(interruptParagraph.indexOf('fencedCode') + 1, 0, ['customBlocks'])
  interruptList.splice(interruptList.indexOf('fencedCode') + 1, 0, ['customBlocks'])
  interruptBlockquote.splice(interruptBlockquote.indexOf('fencedCode') + 1, 0, ['customBlocks'])
}
StaloneLab commented 4 years ago

Ok, I'll start from the beginning:

  1. the plugin you're trying to modify is a remark plugin, hence it parses Markdown and must return valid MDAST, which is a syntax tree for Markdown;
  2. obviously, no Markdown changes are made here, so the MDAST shouldn't change; where does the magic happens, then?
  3. technically, one would expect us to write a remark plugin, to transform custom blocks into a specific MDAST syntax, and a second plugin, but this time for remark-rehype, transforming our custom MDAST into HAST, an syntax tree for HTML;
  4. this process is straightforward, but quite boring, and not so convenient for end-users, so we're relying on a special remark feature, that simply allows to write a single remark plugin with HAST embedded inside;
  5. the HAST is "hidden" inside the data property of an MDAST node, it consist mainly of:
    1. hName, transformed into the tagName property of the HAST element being created;
    2. hProperties, containing the HAST properties; if you know client-side Javascript, the accessible properties are the same one that make the DOM.

In our code, three elements MDAST nodes are generated (hence three HAST elements also):

If you want to customize style on the block, you'll need to allow customization of the three elements separately. What we are doing for classes is not enough, because the given class is only applied to the parent element; for style, you'll need to take a configuration object like:

{
heading: "heading-style",
content: "content-style",
parent: "parent-style"
}

And then applying the corresponding style to each element. As I'm writing this, I'm realizing this is likely not such a good thing to have; could you please explain a bit more why you couldn't use classes with CSS styling instead for your usage?

About your code, styleName is not a valid DOM property, but you could definitely use style. If you're using the plugin with a sanitizer, make sure the style is not filtered; next, you will be able to transform your single-string style parameter into an object, as mentioned above, and apply to each of the three blocks, not only the parent one.

KaptianCore commented 4 years ago

is this good?

const spaceSeparated = require('space-separated-tokens')

function escapeRegExp (str) {
  return str.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, 'g'), '\\$&')
}

const C_NEWLINE = '\n'
const C_FENCE = '|'

function compilerFactory (nodeType) {
  let text
  let title

  return {
    blockHeading (node) {
      title = this.all(node).join('')
      return ''
    },
    blockBody (node) {
      text = this.all(node).map(s => s.replace(/\n/g, '\n| ')).join('\n|\n| ')
      return text
    },
    block (node) {
      text = ''
      title = ''
      this.all(node)
      if (title) {
        return `[[${nodeType} | ${title}]]\n| ${text}`
      } else {
        return `[[${nodeType}]]\n| ${text}`
      }
    },
  }
}

module.exports = function blockPlugin (availableBlocks = {}) {
  const pattern = Object
    .keys(availableBlocks)
    .map(escapeRegExp)
    .join('|')

  if (!pattern) {
    throw new Error('remark-custom-blocks needs to be passed a configuration object as option')
  }

  const regex = new RegExp(`\\[\\[(${pattern})(?: *\\| *(.*))?\\]\\]\n`)

  function blockTokenizer (eat, value, silent) {
    const now = eat.now()
    const keep = regex.exec(value)
    if (!keep) return
    if (keep.index !== 0) return
    const [eaten, blockType, blockTitle] = keep

    /* istanbul ignore if - never used (yet) */
    if (silent) return true

    const linesToEat = []
    const content = []

    let idx = 0
    while ((idx = value.indexOf(C_NEWLINE)) !== -1) {
      const next = value.indexOf(C_NEWLINE, idx + 1)
      // either slice until next NEWLINE or slice until end of string
      const lineToEat = next !== -1 ? value.slice(idx + 1, next) : value.slice(idx + 1)
      if (lineToEat[0] !== C_FENCE) break
      // remove leading `FENCE ` or leading `FENCE`
      const line = lineToEat.slice(lineToEat.startsWith(`${C_FENCE} `) ? 2 : 1)
      linesToEat.push(lineToEat)
      content.push(line)
      value = value.slice(idx + 1)
    }

    const contentString = content.join(C_NEWLINE)

    const stringToEat = eaten + linesToEat.join(C_NEWLINE)

    const potentialBlock = availableBlocks[blockType]
    const titleAllowed = potentialBlock.title &&
      ['optional', 'required'].includes(potentialBlock.title)
    const titleRequired = potentialBlock.title && potentialBlock.title === 'required'

    if (titleRequired && !blockTitle) return
    if (!titleAllowed && blockTitle) return

    const add = eat(stringToEat)
    if (potentialBlock.details) {
      potentialBlock.containerElement = 'details'
      potentialBlock.titleElement = 'summary'
    }

    const exit = this.enterBlock()
    const contents = {
      type: `${blockType}CustomBlockBody`,
      data: {
        hName: potentialBlock.contentsElement ? potentialBlock.contentsElement : 'div',
        hProperties: {
          className: 'custom-block-body',
          style: ['body-style', ...styleList],
        },
      },
      children: this.tokenizeBlock(contentString, now),
    }
    exit()

    const blockChildren = [contents]
    if (titleAllowed && blockTitle) {

      const titleElement = potentialBlock.titleElement ? potentialBlock.titleElement : 'div'
      const titleNode = {
        type: `${blockType}CustomBlockHeading`,
        data: {
          hName: titleElement,
          hProperties: {
            className: 'custom-block-heading',
            style: ['style-title', ...styleList],
          },
        },
        children: this.tokenizeInline(blockTitle, now),
      }

      blockChildren.unshift(titleNode)
    }
    const styleList = spaceSeparated.parse(potentialBlock.styles || '')
    const classList = spaceSeparated.parse(potentialBlock.classes || '')

    return add({
      type: `${blockType}CustomBlock`,
      children: blockChildren,
      data: {
        hName: potentialBlock.containerElement ? potentialBlock.containerElement : 'div',
        hProperties: {
          className: ['custom-block', ...classList],
          style: ['style-block', ...styleList],
        },
      },
    })
  }

  const Parser = this.Parser

  // Inject blockTokenizer
  const blockTokenizers = Parser.prototype.blockTokenizers
  const blockMethods = Parser.prototype.blockMethods
  blockTokenizers.customBlocks = blockTokenizer
  blockMethods.splice(blockMethods.indexOf('fencedCode') + 1, 0, 'customBlocks')
  const Compiler = this.Compiler
  if (Compiler) {
    const visitors = Compiler.prototype.visitors
    if (!visitors) return
    Object.keys(availableBlocks).forEach(key => {
      const compiler = compilerFactory(key)
      visitors[`${key}CustomBlock`] = compiler.block
      visitors[`${key}CustomBlockHeading`] = compiler.blockHeading
      visitors[`${key}CustomBlockBody`] = compiler.blockBody
    })
  }
  // Inject into interrupt rules
  const interruptParagraph = Parser.prototype.interruptParagraph
  const interruptList = Parser.prototype.interruptList
  const interruptBlockquote = Parser.prototype.interruptBlockquote
  interruptParagraph.splice(interruptParagraph.indexOf('fencedCode') + 1, 0, ['customBlocks'])
  interruptList.splice(interruptList.indexOf('fencedCode') + 1, 0, ['customBlocks'])
  interruptBlockquote.splice(interruptBlockquote.indexOf('fencedCode') + 1, 0, ['customBlocks'])
}
StaloneLab commented 4 years ago

Do you know what ['body-style', ...styleList] does?

EDIT: also, I'll reask this: could you please explain a bit more why you couldn't use classes with CSS styling instead for your usage?

KaptianCore commented 4 years ago

I'm closing this issue as I no longer require it, but it was for coloured text.