Adding Markdown tag selectors

So I was trying to come up with a new theme to (probably mistakenly) dive further into Joplin theming when I realised that one of the things I personally would really like would be to theme the markdown tags themselves independently of the span with the content.
So after initially seeing if it could be done with existing selectors (it doesn't seem possible) I did bit of digging around and I found exactly what was needed so implemented it into a dev build of Joplin (basically just chucked a switch into JoplinMarkdown) - this is what it looks like for some of the tags I've styled:

image

Now the issue is that I've not good a good enough grasp on the code or project to know exactly what to do with it. Or at least not be certain I'm not about to break something fundamental.

Implement into Joplin itself

This is what I've done on my build, and it seems to work well and I've not seen any breaking issues.

  • Existing CSS should still be compatible as the addition simply creates a more specific selector, the existing CSS selectors should still target them as they did before
  • Will allow for more (native) syntax theming
  • I don't know if the addition of the extra spans created will upset anything else in Joplin

Implement as a standalone plugin

I did try this and failed miserably so I would need some advice if this is a preferred route

  • Would essentially be a dependency for any themes using the extra selectors unless this was also implemented as part of those (plugin) themselves
  • Has the same positives + negatives as above except it would be only the plugin that is broken, not something more fundamental in Joplin

Implement in a different way entirely

@CalebJohn's richmarkdown plugin already does this but via a very different method in that it uses regex to identify the selectors + create new CSS classes for them (please correct me if I'm speaking out of my derrière)

  • Means that the full plugin is required just for a tiny bit of extra theming flavour (or re-invent the wheel by stealing adapting bits of richmarkdown as its own plugin - with the same notes as above)
  • Is there a performance benefit to be had in having the tags created natively rather than via regex?

In my mind such a change benefits both sides of the previously discussed "markdown as code" topic (i.e. some prefer to see plain or mildly hybrid markdown code complete with syntax highlighting rather than WYSIWYG-ish style à la richmarkdown, typora, marktext etc.)
In one camp it means you can use CSS to mark the formatting tags as light grey , smaller or even display:none) whereas for the other camp you can style them so they are even more obvious or so that the content clashes with the style of the tags

I'd be interested to hear any thoughts on this, if people think its a good idea then I'll submit a PR (not that it was a substantial coding challenge to +26 Joplin's source) or understand what tests or cases need to be run first to prove things are OK, if not then I'm more than interested to understand what the issues are.

Which switch did you turn on to get this result? I've tried it before and couldn't find out. If it doesn't cause any regression I think it would indeed be a good idea to enable it.

I added highlightFormatting: true, to CodeMirror.defineMode() in useJoplinMode.ts
CM defaults it to off CodeMirror: Markdown mode

export default function useJoplinMode(CodeMirror: any) {

	CodeMirror.defineMode('joplin-markdown', (config: any) => {
		const markdownConfig = {
			name: 'markdown',
			taskLists: true,
			strikethrough: true,
			emoji: Setting.value('markdown.plugin.emoji'),
	------> highlightFormatting: true,
			tokenTypeOverrides: {
				linkText: 'link-text',
			},
		};
...

Examples of CSS for the new selectors:

span.cm-em {
  color: var(--spurple) !important;
}
span.cm-formatting.cm-formatting-em.cm-em {
  color: var(--smono1) !important;
}
span.cm-strong {
  color: var(--sorange1) !important;
}
span.cm-formatting.cm-formatting-strong.cm-strong {
  color: var(--smono1) !important;
}

That's great if we can simply set this and it works. Did you notice anything off with the editor? @CalebJohn, are you familiar with this option and do you think we can safely enable it without breaking something?

I can't say I've noticed any problems so far, it seems to behave just as the editor normally does but I've not done a huge amount with it just yet (the need to cook dinner tore me away from playing). I still need to have a look at some more of the extended Joplin markdown plugins to see how they behave.

I would publish it to my repo for people to play with (although I still can/will, I still need to grab the latest commits) but it is only a small line within the above file, literally nothing else. I originally tried it just by editing the codemirror node package itself as a proof of concept.

I haven't tested it with any of the recommended plugins to see if it breaks them just yet either.

I am familiar with it, and did turn it on for testing once. If I remember correctly it did break some styling (maybe it was custom styling though). It's been in the back of my mind for awhile to try again and if @Daeraxa is already on it then I'll be happy to help as needed.

I don't know if the addition of the extra spans created will upset anything else in Joplin

I think they will, but it's probably something that can be worked around

  • Is there a performance benefit to be had in having the tags created natively rather than via regex?

Yes, it will be faster to have this done natively. The regex are really slow.

1 Like

So I created some notes that should in theory contain all markdown elements officially supported by Joplin (attached) - although most of the elements in the markdown plugins doc seem mostly to do with the html render rather than the codemirror editor display.
Markdown tester (CommonMark + Joplin Extra).md (2.3 KB)
Markdown tester (Joplin Markdown plugins).md (1.5 KB)

Using the default theme I don't see anything obviously wrong (although admittedly I'm unfamiliar with how many elements should be displaying, particularly many of the additional plugins but also katex, mermaid etc.)

An example of the old and new html for an h1:

<!--old-->
<pre class=" CodeMirror-line " role="presentation"><span role="presentation" style="padding-right: 0.1px;"><span class="cm-header cm-header-1"># ATX Header 1</span></span></pre>
<!--new-->
      <pre class=" CodeMirror-line "
        role="presentation"><span role="presentation" style="padding-right: 0.1px;"><span class="cm-formatting cm-formatting-header cm-formatting-header-1 cm-header cm-header-1"># </span><span class="cm-header cm-header-1">ATX Header 1</span></span></pre>

The main issue I've found so far is a (potentially) relatively minor one in regards to how the inline codeblock is rendering:
Old:
image
New:
image
The style for this element is applying borders to all 3 elements (tags x2, content x1) and I'm not sure how to fix this currently. I can set the middle content element to have border-style-left & border-style-right as none as well as removing the border-radius but I don't know how to target the two tag elements independently to set one with no right border and one with no left border (+ remove the corresponding radii). I'm probably missing some kind of nth-child magic here that I'm not familiar with.

A couple of the plugin elements are not recognised at all (which isn't too surprising), for example ==mark== and ++insert++ tags are not separated. Deflist looks a bit odd both before and after the change so I don't think any of those are a regression.

Sync scroll seems to be dealing with it just fine unless @ken1kob can think of a case where this might be broken?

Just as an aside, I played around with this, even just limiting the display:none to non-selected lines, and I had some undesirable results. I would place the cursor into a line that had hidden tags and when the tags appeared, the cursor was in the wrong place. But if someone could get this working properly it would be brilliant.

1 Like

I think sync scroll is not affected. It relies on block tags such as <p>, <div> and <table>. If such tags and their class map-to-line and attribute source-line are kept, sync scroll works.

Ref: renderer/MdToHtml/rules/source_map.ts

1 Like

An update on the inline code block, I think I've made..... something...

image

On the surface I think it looks correct but underneath there be dragons...

CodeMirror.tsx

/* Mark all inline codes with no right border and only round the left two corners*/
div.CodeMirror span.cm-comment.cm-jn-inline-code.cm-jn-monospace:not(.cm-search-marker):not(.cm-fat-cursor-mark):not(.cm-search-marker-selected):not(.CodeMirror-selectedtext) {
  border-top: 1px solid ${theme.codeBorderColor};
  border-bottom: 1px solid ${theme.codeBorderColor};
  border-right: none;
  border-left: 1px solid ${theme.codeBorderColor};
  background-color: ${codeBackgroundColor};
  margin-left: 0px;
  margin-right: 0px;
  border-radius: 0.25em 0 0 0.25em;
}
/* Use adjacent selector to select the content area + remove borders */
span.cm-formatting.cm-formatting-code.cm-comment.cm-jn-inline-code:not(.cm-search-marker):not(.cm-fat-cursor-mark):not(.cm-search-marker-selected):not(.CodeMirror-selectedtext) + span.cm-comment.cm-jn-inline-code:not(.cm-search-marker):not(.cm-fat-cursor-mark):not(.cm-search-marker-selected):not(.CodeMirror-selectedtext){
    border-top: 1px solid ${theme.codeBorderColor};
    border-bottom: 1px solid ${theme.codeBorderColor};
    border-right: none;
    border-left: none;
    margin-right: 0px;
    margin-left: 0px;
    border-radius: 0px;
}

/* Use adjacent selector to select the element following the inline code + place right side borders + radii*/
span.cm-comment.cm-jn-inline-code:not(.cm-search-marker):not(.cm-fat-cursor-mark):not(.cm-search-marker-selected):not(.CodeMirror-selectedtext) + span.cm-formatting.cm-formatting-code.cm-comment.cm-jn-inline-code:not(.cm-search-marker):not(.cm-fat-cursor-mark):not(.cm-search-marker-selected):not(.CodeMirror-selectedtext){
    border-top: 1px solid ${theme.codeBorderColor};
    border-bottom: 1px solid ${theme.codeBorderColor};
    border-right: 1px solid ${theme.codeBorderColor};
    border-left: none;
    margin-left: 0px;
    border-radius: 0 .25em .25em 0;
}

Yes it is overly verbose with a bunch of redundant selectors but it shows the overall approach - please, if anyone has a better way of handling this then let me know as this is somewhat unwieldy to say the least...

1 Like