Implement live markdown rendering using slate.js - javascript

I'm writing a markdown text editor using slate.js. I'm trying to implement the following live-rendering effect (from Typora):
As you can see,
When I'm typing, the text is turning to bold automatically.
When I hit the space key, the four asterisks disappeared, only the text itself is visible.
When I focus the cursor back to the text, the asterisks shows up again (so I can modify them).
I've already implemented the first item thanks to the example of MarkdownPreview, here is the code of it (take from the slate repository):
import Prism from 'prismjs'
import React, { useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Text, createEditor, Descendant } from 'slate'
import { withHistory } from 'slate-history'
import { css } from '#emotion/css'
// eslint-disable-next-line
;Prism.languages.markdown=Prism.languages.extend("markup",{}),Prism.languages.insertBefore("markdown","prolog",{blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},code:[{pattern:/^(?: {4}|\t).+/m,alias:"keyword"},{pattern:/``.+?``|`[^`\n]+`/,alias:"keyword"}],title:[{pattern:/\w+.*(?:\r?\n|\r)(?:==+|--+)/,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#+.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:/(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^\*\*|^__|\*\*$|__$/}},italic:{pattern:/(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/,lookbehind:!0,inside:{punctuation:/^[*_]|[*_]$/}},url:{pattern:/!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/,inside:{variable:{pattern:/(!?\[)[^\]]+(?=\]$)/,lookbehind:!0},string:{pattern:/"(?:\\.|[^"\\])*"(?=\)$)/}}}}),Prism.languages.markdown.bold.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.italic.inside.url=Prism.util.clone(Prism.languages.markdown.url),Prism.languages.markdown.bold.inside.italic=Prism.util.clone(Prism.languages.markdown.italic),Prism.languages.markdown.italic.inside.bold=Prism.util.clone(Prism.languages.markdown.bold); // prettier-ignore
const MarkdownPreviewExample = () => {
const renderLeaf = useCallback(props => <Leaf {...props} />, [])
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
const decorate = useCallback(([node, path]) => {
const ranges = []
if (!Text.isText(node)) {
return ranges
}
const getLength = token => {
if (typeof token === 'string') {
return token.length
} else if (typeof token.content === 'string') {
return token.content.length
} else {
return token.content.reduce((l, t) => l + getLength(t), 0)
}
}
const tokens = Prism.tokenize(node.text, Prism.languages.markdown)
let start = 0
for (const token of tokens) {
const length = getLength(token)
const end = start + length
if (typeof token !== 'string') {
ranges.push({
[token.type]: true,
anchor: { path, offset: start },
focus: { path, offset: end },
})
}
start = end
}
return ranges
}, [])
return (
<Slate editor={editor} value={initialValue}>
<Editable
decorate={decorate}
renderLeaf={renderLeaf}
placeholder="Write some markdown..."
/>
</Slate>
)
}
const Leaf = ({ attributes, children, leaf }) => {
return (
<span
{...attributes}
className={css`
font-weight: ${leaf.bold && 'bold'};
font-style: ${leaf.italic && 'italic'};
text-decoration: ${leaf.underlined && 'underline'};
${leaf.title &&
css`
display: inline-block;
font-weight: bold;
font-size: 20px;
margin: 20px 0 10px 0;
`}
${leaf.list &&
css`
padding-left: 10px;
font-size: 20px;
line-height: 10px;
`}
${leaf.hr &&
css`
display: block;
text-align: center;
border-bottom: 2px solid #ddd;
`}
${leaf.blockquote &&
css`
display: inline-block;
border-left: 2px solid #ddd;
padding-left: 10px;
color: #aaa;
font-style: italic;
`}
${leaf.code &&
css`
font-family: monospace;
background-color: #eee;
padding: 3px;
`}
`}
>
{children}
</span>
)
}
const initialValue: Descendant[] = [
{
type: 'paragraph',
children: [
{
text:
'Slate is flexible enough to add **decorations** that can format text based on its content. For example, this editor has **Markdown** preview decorations on it, to make it _dead_ simple to make an editor with built-in Markdown previewing.',
},
],
},
{
type: 'paragraph',
children: [{ text: '## Try it out!' }],
},
{
type: 'paragraph',
children: [{ text: 'Try it out for yourself!' }],
},
]
export default MarkdownPreviewExample
My question is, how can I implement the second and third items? I've been thinking about it for a long time, but didn't find any good way to achieve them.

The good news:
The original text is saved unharmed (somewhere in your application)
Slate.js is rendering a new element for each style. thanks to the live demonstration link you've posted it is clear that the DOM is saving the state of your markdown (which suggest that we can add manipulations on-top of slate if required)
The bad news:
Implementing an editor is a headache. Using an existing one is not always as intuitive and easy as we expected. That's why I love the Monaco editor project so much (a free editable editor that gives me the experience of VS code for free? yes please!)
What we can do?
the easy way:
Use Monaco editor in our project. From my experience - it is better in terms of coding and formatting technical texts (and maybe this is the engine behind VS code)
The not-so-easy way:
We can make a simpler implementation - a panel with the raw text and another panel for preview (by the way - this is exactly what happens when you edit markdown files in VS code anyways). That way we can always render the view to reflect our most recent changes. For starters - you can use this project to do it and add tweaks to it so suit your demands
The hard way:
Using slate.js commands (read this doc for the general concept + useful examples). This approach will let slate handle the load but requires you to deep-dive into that project as well. Note that you can register custom commands and queries to suit your need without breaking your work pipeline (read this)
The insane way:
Try to override slate by using custom events on-top of rendered elements. This can be tricky but achievable if you love to play with projects internals and inject values on the fly. Not very recommended though
My Recommendation
Create a custom command that will apply the markdown style you want (for instance: bold)
Use slate handler for tracking the space key hit and use your command
function onKeyDown(event, editor, next) {
if (event.key == 'Enter') {
// TODO: add markdown style
editor.applyMarkdownBold() // applyMarkdownBold is a made-up name for your custom command
} else {
return next()
}
}
I know - this example will apply bold style to all text but - if you combine it with a range selection you will get the style you need for the relevant area in you editor (again - docs for the rescue)
From this point - applying a specific selection via key press or adding the markdown characters by clicking it (using an event + query + command) is only a matter of time

Related

Approaches for Styling Individual Letters in a Web Component

Let's say I want to create a custom element which bolds every other character. For example, <staggered-bold>Hello</staggered-bold> would become "Hello, where the H, l, and o are all bolded.
There's no nth-letter CSS selector, so as far as I know the only way to achieve this effect is to wrap each individual character with a span programmatically. To do that, I have an implementation that clones the text content into the Shadow Dom, so that the child content as specified by the user is not changed.
Unfortunately, by doing so, something like <staggered-bold><span class="red">red</span></staggered-bold> no longer works, because by cloning the content into the Shadow Dom, the class CSS declarations for the wrapped span no longer apply.
Here's a proof-of-concept implementation, showcasing that the red and blue text are in fact not red and blue:
customElements.define('staggered-bold', class extends HTMLElement {
constructor() {
super()
this
.attachShadow({ mode: 'open' })
.appendChild(document.getElementById('staggered-bold').content.cloneNode(true))
}
connectedCallback() {
// this is a shadow dom element
const text = this.shadowRoot.getElementById('text')
this.shadowRoot.querySelector('slot').assignedNodes().forEach(node => {
const content = node.textContent.split('').map((char) => {
return `<span class="char">${char}</span>`
}).join('')
const newNode = node.nodeType === Node.TEXT_NODE ? document.createElement('span') : node.cloneNode(true)
newNode.innerHTML = content
text.appendChild(newNode)
})
}
})
.red { color: red; }
.blue { color: blue; }
<p><staggered-bold>Some text</staggered-bold></p>
<p><staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold></p>
<template id="staggered-bold">
<style>
.hide { display: none; }
.char:nth-child(odd) {
font-weight: bold;
}
</style>
<span class="hide"><slot></slot></span>
<span id="text"></span>
</template>
My question is this: what is a good approach to styling each character in a custom element while preserving characteristics provided in the light dom?
One approach I've considered is to manipulate the light dom directly, but I have been avoiding that since I think of the light dom as being in full control of the usage-site (ie. things get complicated very quickly if external JS is manipulating the child of staggered-bold). I'm open to being convinced otherwise, especially there's no real alternative.
I've also considered cloning the content into a named slot so that the original text is preserved, and yet the content continues to live in the light dom. However, I feel like this is still kind of icky for the same reason as the previous paragraph.
You can't have the cake and eat it
Global CSS does NOT style shadowDOM (unless you use CSS properties)
Easier to not use shadowDOM at all.
With an extra safeguard: store the state so the element is properly redrawn on DOM moves.
Note: The setTimeout is always required,
because the connectedCallback fires early on the opening tag;
there is no parsed (innerHTML) DOM yet at that time.
So you have to wait for that DOM to be there.
If you do need a TEMPLATE and shadowDOM, dump the whole .innerHTML to the shadowRoot; but Global CSS still won't style it. Or <slot> it.
Do read: ::slotted CSS selector for nested children in shadowDOM slot
If you go with <slot> consider the slotchange Event
but be aware for an endless loop; changing lightDOM will trigger the slotchange Event again
<staggered-bold>Some text</staggered-bold>
<staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold>
<style>
staggered-bold { display: block; font: 21px Arial }
staggered-bold .char:nth-child(even) { color: blue }
staggered-bold .char:nth-child(odd) { color: red; font-weight: bold }
</style>
<script>
customElements.define('staggered-bold', class extends HTMLElement {
connectedCallback() {
setTimeout(() => { // make sure innerHTML is all parsed
if (this.saved) this.innerHTML = this.saved;
else this.saved = this.innerHTML;
this.stagger();
})
}
stagger(node=this) {
if (node.children.length) {
[...node.children].forEach( n => this.stagger(n) )
} else {
node.innerHTML = node.textContent
.split('')
.map(ch => `<span class="char">${ch}</span>`)
.join('');
}
}
})
document.body.append(document.querySelector("staggered-bold"));//move in DOM
</script>
In the end I attempted a strategy I'm calling the mirror node. The idea is the custom element actually creates an adjacent node within which the split characters are placed.
The original node remains exactly as specified by the user, but is hidden from view
The mirror node actually displays the staggered bold text
The below implementation is incomplete, but gets the idea across:
class StaggeredBoldMirror extends HTMLElement {
constructor() {
super()
}
}
customElements.define('staggered-bold', class extends HTMLElement {
constructor() {
super()
this
.attachShadow({ mode: 'open' })
.appendChild(document.getElementById('staggered-bold').content.cloneNode(true))
}
connectedCallback() {
setTimeout(() => {
const mirror = new StaggeredBoldMirror()
mirror.innerHTML = this.divideIntoCharacters()
this.parentNode.insertBefore(mirror, this)
})
}
divideIntoCharacters = (node = this) => {
return [...node.childNodes].map(n => {
if (n.nodeType === Node.TEXT_NODE) {
return n.textContent
.split('')
.map(ch => `<span class="char">${ch}</span>`)
.join('')
} else {
const nn = n.cloneNode(false)
nn.innerHTML = this.divideIntoCharacters(n)
return nn.outerHTML
}
}).join('')
}
})
customElements.define('staggered-bold-mirror', StaggeredBoldMirror)
.red {
color: red;
}
.blue {
color: blue;
}
staggered-bold-mirror .char:nth-child(odd) {
font-weight: bold;
}
<p><staggered-bold>Some text</staggered-bold></p>
<p><staggered-bold><span class="red">Red</span> <span class="blue">Blue</span></staggered-bold></p>
<template id="staggered-bold">
<style>
.hide { display: none; }
</style>
<span class="hide"><slot></slot></span>
</template>
The vanilla component can be outfitted with a slotchange listener in order to rebuild its mirror whenever its inner content changes. The disconnectedCallback method can also ensure that when one node is removed, the other is too.
Of course, there are downsides to this approach, such has potentially having to also mirror events and the fact that it still manipulates the light dom.
Depending on the use case, either this or Danny's answer works.

binding textContent and overriding dom with svelte

I have a div with contenteditable=true and bind:textContent={value} so it behaves pretty much like a textarea.
The only issue I have with it is that I want to override the content of the div by processing the value, but seems like it is not possible.
To test I wrote this
<div contenteditable="true" bind:textContent={value}>testVal</div>
where value is an exported property of the component.
I kind of expected value to be set to testVal, but instead the div contains the value property.
I sort of understand why this is happening and that what I am doing is sort of an edge case, but is it at all possible to change this behaviour to kind of get a one way binding to value?
and I have tried my "normal" way of creating a one way binding (with some hacks to demonstrate issues):
<div contenteditable="true" on:input={e => value = e.target.textContent}>
{#each (value || "").split("") as part}
{part}
{/each}
</div>
this looks fine, but whenever I change type in the div my input gets multiplied, i.e. if I type e the div gets updated with ee. If I add another e I get eeee
I think the way to go is to use your "normal" way of creating a one way binding. Otherwise, using multiple ways of binding on the same element will conflict.
I used a combination of on:input like you described and, inside of the div, {#html html}
The following example formats each other word in bold as you type (there's some glitch when starting with an empty field):
<script>
import {tick} from "svelte";
let html = '<p>Write some text!</p>';
// for the implementation of the two functions below, see
// https://stackoverflow.com/a/13950376/4262276
let saveSelection = (containerEl) => { /**/ };
let restoreSelection = (containerEl, savedSel) => { /**/ };
let editor;
function handleInput(e){
const savedSelection = saveSelection(editor);
html = e.target.textContent
.split(" ")
.map((t, i) => i % 2 === 0
? `<span style="font-weight:bold">${t}</span>`
: t
)
.join(" ");
tick().then(() => {
restoreSelection(editor, savedSelection);
})
}
</script>
<div
bind:this={editor}
contenteditable="true"
on:input={handleInput}
>{#html html}</div>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>

Best way to share functionality but have different styles?

I am building a Next.js website and I am running into this issue:
I have 3 forms which work essentially the same way but they all look different. I would like to build the actual form only once but be able to somehow style it in different ways without duplicating too much code and doing things the "right way".
The solution I have come up with is to simply give the form a class and style its children via classic CSS. This works but feels "wrong" considering the rest of the project is using Styled-components. I would like to find a solution that follows the principles behind SC instead of adding global styles to the app.
This is what the code currently looks like:
// form.js
export default Form = () => {
/* ... hooks and stuff ... */
return (
<form>
/* ... */
</form>
)
}
// styled-form-1.js
import Form from './Form.js'
export default Form = () => {
return (
<Form className="styled-1" />
)
}
// globalStyles.css
.styled-1 form { /* ... */ }
.styled-1 input { /* ... */ }
.styled-1 button { /* ... */ }
.styled-2 form { /* ... */ }
.styled-2 input { /* ... */ }
.styled-2 button { /* ... */ }
As I said... This works fine but I want to do it the styled-components way. I just cannot find that solution.
You can pass function to styled-component's template literal to adapt it based on props. For example:
const Button = styled.button`
background: ${props => props.primary ? "red" : "blue"};
color: ${props => props.primary ? "blue" : "red"};
font-size: 1em;
padding: 1em;
`;
render(
<div>
<Button>Normal</Button>
<Button primary>Primary</Button>
</div>
);
By this way you can create single styled-components for your form and change it's styles based on the props. For more, check official Doc

How do I get my function to find my input arguments?

I am creating a function that would format my input text using predefined styles.
const icons = {
user: {'icon_cod': '%c  %c', 'font_icon': 'font-family:"Font Awesome 5 Free"; font-weight: 900;'} // declare my icon
}
const txt_styles = {
texto: {'style': 'color:red; font-family: "Open Sans"; font-weight: 700; font-size: 15px;', // Format my text and icon
'icon_style': 'color:blue;'}
}
function icon_construct(icon_chosen,text,style) {
txt_formatted = ''
if (icon_chosen in icons && style in txt_styles) { // Check the input
txt_formatted = icons.icon_chosen['icon_cod'] + text, icons.icon_chosen['font_icon'] + txt_styles.style['icon_style'], txt_styles.style['style']
} return txt_formatted // Returns formatted text
}
console.log(icon_construct('user','Hello World','body'));
However the exit is returns this error to me:
The expected output would be this
To exemplify how formatting happens
//Run in the browser console
console.log ('%c Red %c Blue','color: red; ','color: blue; ')
The styles follow the order in which the elements were declared
Function does not find the value of the argument
Try this (Note that snippet cannot print design to console, take a look at the devtools to see the result):
const
icons = {
user: {
iconCode: '%c  %c',
fontIcon: 'font-family: "Font Awesome 5 Free"; font-weight: 900;'
}
},
textStyles = {
text: {
style: 'color: red; font-family: "Open Sans"; font-weight: 700; font-size: 15px;',
iconStyle: 'color: blue;'
}
},
iconConstruct = (iconChosen, text, style) =>
iconChosen in icons && style in textStyles ? [
icons[iconChosen].iconCode + text,
icons[iconChosen].fontIcon + textStyles[style].iconStyle,
textStyles[style].style
] : [];
console.log(...iconConstruct('user', 'Hello World', 'text'));
There is no prop named icon_chosen. You need the square bracket syntax here: txt_formatted = icons.icon_chosen.
It is easy to understand like this, assume you have an object as given:
obj = { 1:{'color':'black','height':'20px'}
2:{'color':'white','height':'60px'}
}
So if you want access the color property it can be done by
obj[1].color
in your case:
icons[icon_chosen].icon_cod
there is no prop icon_chosen for icon
here icon_chosen contain value user
so by using icon[icon_chosen] you will have user object value.
try below function
function icon_construct(icon_chosen,text,style) {
if ( icons.hasOwnProperty(icon_chosen) && txt_styles.hasOwnProperty(style)) { // Check the input
console.log(icons[icon_chosen]['icon_cod'] + text, icons[icon_chosen]['font_icon'] + txt_styles[style]['icon_style'], txt_styles[style]['style'])
}
}
working example (user icon missing - i guess for css is missing in my local )

Disable visual effects of a CSS class

On a site a CSS class is periodically added to an element by JavaScript.
I'd like not to show the visible effect of that class. In other words what I need is an effect similar to switching the class off in the development console of the browser. Let the class exist but without any consequences.
I understand that I can catch events and remove the class when it appears.
But maybe there is just a more simple way in my case?
In other words: there is a CSS class, I would like it to be present but without any visual effects. If it is impossible, that will also be an answer.
You could use something like this. Access the document's stylesheets and apply some sort of regex matching to figure out which rules are associated with the class in question. Then simply unset the styling on the rules. Note that just because the class name is found in the stylesheet rule doesn't mean it is the element being affected by the styles...but this should get you going in the right direction.
function removeClassStyling(clazz) {
var classRegex = new RegExp('\\.'+clazz.toLowerCase()+'\\b','i')
for (var s=0; s<document.styleSheets.length; ++s) {
var sheet = document.styleSheets[s];
for(var r=0; r<sheet.cssRules.length; ++r) {
var rule = sheet.cssRules[r];
if(rule.selectorText && rule.selectorText.match(classRegex)) {
var properties = Object.keys(rule.style);
for(var p=0; p<properties.length; ++p){
if(rule.style[properties[p]]) rule.style[properties[p]] = "";
}
console.log('removed styling for "'+clazz+'"');
}
}
}
}
setTimeout(function(){ removeClassStyling('unwanted-class') }, 1500)
.unwanted-class {
border: 1px solid red;
}
<div class="unwanted-class"> Test </div>
This should do the trick in most circumstances. I imagine there are circumstances that would evade this, but I can't think of them.
Basically you need to iterate document.styleSheets collection, then iterate each rule contained within and compare the CSSStyleRule.selectorText for each rule against a regular expression.
Regular expressions can be faulty, so I've included a check against an element with the supplied class name using the Element#matches() method. That method can also provide false positives in the case where the element matches some other part of the selector, so the two together should reasonably prevent any false positives.
Once you have a list of all the CSS rules that apply to a given class, you can simply delete them all. This can be done in the same step as finding them, but I've done it separately in for example's sake.
function findClassRules(name) {
const element = document.createElement('div')
element.classList.add(name)
const regex = new RegExp(`\\\.${ name }([^\w]|$)`, 'i')
const test = {
rule: rule => {
if('cssRules' in rule) {
return test.sheet(rule)
} else if('selectorText' in rule) {
const selector = rule.selectorText
return selector.match(regex) && element.matches(selector) && rule
}
},
sheet: sheet => {
const rules = Array.from(sheet.cssRules, test.rule).filter(Boolean)
return rules.length && { sheet, rules }
}
}
return Array.from(document.styleSheets, test.sheet).filter(Boolean)
}
function processSheet({ sheet, rules }) {
rules.forEach(rule => {
if('rules' in rule) {
processSheet(rule)
} else {
sheet.deleteRule(rule)
console.log(`Removed: ${ rule.cssText }`)
}
})
}
document.getElementById('clean').addEventListener('click', event => {
findClassRules('test').forEach(processSheet)
}, false)
.test { padding: 5px }
.test2 { padding: 10px }
#media screen {
.test { margin: 15px }
}
<p class="test">Hello world!</p>
<button id="clean">Remove CSS</button>
<style type="text/css">
.test { color: red }
</style>
<style type="text/css">
.test { border: 1px solid red }
</style>
How about you comment the class in your css file

Categories

Resources