showdownjs / showdown

A bidirectional Markdown to HTML to Markdown converter written in Javascript
http://www.showdownjs.com/
MIT License
14.26k stars 1.56k forks source link

Multiline code blocks and code tags (line numbering) #887

Closed ZTiKnl closed 2 years ago

ZTiKnl commented 2 years ago

Update: Solution here

Is it possible to have showdown add <code> tags for every line in a multiline code block?

How it works now (not sure how to format example properly, sry):

```
This is a test
This is a test too
This is a test three
```

Results in:

<pre><code>This is a test
This is a test too
This is a test three</code></pre>

What I am hoping to get is:

<pre><code>This is a test</code>
<code>This is a test too</code>
<code>This is a test three</code></pre>

Reason: I have some CSS code that allows for line numbering in code blocks This works using a counter for <code> tags, and a reset at each <pre> tag Since all lines are placed in a single <code> tag, it results in counting only a single line

My working CSS (resulting in only recognizing a single line):

pre {
    counter-reset: line-numbering;
    font-family: Menlo, Monaco, monospace;
    background-color: #333;
    padding: 5px;
    padding-left: 15px;
    color: #CCC;
    border-radius: 3px;
    word-break: keep-all;
    white-space: pre-wrap;
}
code:before {
    content: counter(line-numbering);
    counter-increment: line-numbering;
    padding-right: 1em;
    width: 1.5em;
    text-align: right;
    opacity: 0.5;
}

I have tried to find a solution using google and the issue tracker, but wasn't able to find it. Apologies if I have overlooked something obvious ;)

tivie commented 2 years ago

Out of the box, showdown does not support that functionality. However, it's very easy to create an extension that does that for you.

You can use a listener extension, listening to the makehtml.codeBlocks.after event and change each line in the output prepending <code> and appending </code>

ZTiKnl commented 2 years ago

Thank you for your quick response, and glad to hear I'm not asking for the impossible!

I have one follow-up question:
Is there any documentation or a working example/extension making use of the listener event? edit: found that here, testing when I get back home! Perhaps such an example could be added to the wiki for other (blind) people like me ;)

According to the extension wiki it is an upcoming feature, and unless I misunderstand, it is a seperate entity from the lang & output sub-extentions.

To be clear, I'm not asking to make the extention for me, happy to do the work myself, but am not sure how the syntax of a listener is supposed to work.

ZTiKnl commented 2 years ago

Almost there (I think):

Creating extension:

var codetageachline = function () {
  console.log('test'); // works, runs once each script execution, seems alright to me so far

  var myext1 = {
    type: 'listener',
    listeners: {
      'makehtml.codeBlocks.after': function (event, text, converter, options, globals) {
        console.log('test: ' + text); // doesnt work

        text = text.replace(/\Memory/g, 'jbsgfjfdhbg');
        return text;
      }
    }
  };
  return myext1;
}

Initializing showdown with options and load & convert content:

        var markdownconverter = new showdown.Converter({extensions: [codetageachline], ellipsis:false});
        markdownconverter.setOption('simpleLineBreaks', 'true');
        markdownconverter.setOption('omitExtraWLInCodeBlocks', 'true');
        markdownconverter.setOption('parseImgDimensions', 'true');
        markdownconverter.setOption('strikethrough', 'true');
        markdownconverter.setOption('tables', 'true');
        markdownconverter.setOption('tasklists','true');

          var input = fs.readFileSync(filePath, 'utf8');
          var html = markdownconverter.makeHtml(input, {mode: 'nonAsciiPrintable', level: 'html5'});

File has the word Memory in it multiple times, but doesn't seem to even reach that point. The console.log('test'); at the start of the extension does work, so the extension is being triggered.

Further testing/experimenting required, to be continued!

tivie commented 2 years ago

codetageachline should return an array of extensions:

return [myext1];
ZTiKnl commented 2 years ago

Updated, but afraid to say it doesn't seem to solve it

var codetageachline = function () {
  console.log('test1'); // works
  var myext1 = {
    type: 'listener',
    listeners: {
      'makehtml.codeBlocks.after': function (event, text, converter, options, globals) {
        console.log('test2: ' + text); // doesn't work, would at least expect 'test2: ' on console log here
        sdjnfkjnfdsdkf; // undefined, doesn't trigger error
        text = text.replace(/e/g, 'jbsgfjfdhbg');
        return text;
      }
    }
  };
  return [myext1];
}

It appears as the function linked to the listener is never triggered

The .md file I am reading contains code blocks (in markdown syntax) When viewing the resulting HTML, there is a pre/code block in the source code at the right spot, but no modifications are made inside it.

I have changed the regex to be as simple as I can think of, replace all letter e with a string, to be sure the regex isnt where I'm failing, but no luck so far :x

ZTiKnl commented 2 years ago

I tried another approach, using a seperate extension file, by making a copy/clone of the prettify extension:

The replace isn't being executed, neither is the console.log just in front of it.
Anyway, just reporting my progress / documenting attempts, I will continue to test and experiment (as free time allows me to)

Main script:

const showdown = require('showdown');
const codetagseachline = require('./codetagseachline.js');
showdown.extension('codetagseachline', codetagseachline);

var markdownconverter = new showdown.Converter({extensions: ['codetagseachline']});

var input = 'Test line 1\n' + 'Code block line 1\n' + '```Code block line 2\n' + 'Code block line 3\n```' + 'Test line 5';
var html = markdownconverter.makeHtml(input, {mode: 'nonAsciiPrintable', level: 'html5'});
console.log(html);

codetagseachline.js:

(function (extension) {
  'use strict';

  if (typeof showdown !== 'undefined') {
    // global (browser or nodejs global)
    extension(showdown);
  } else if (typeof define === 'function' && define.amd) {
    // AMD
    define(['showdown'], extension);
  } else if (typeof exports === 'object') {
    // Node, CommonJS-like
    module.exports = extension(require('showdown'));
  } else {
    // showdown was not found so we throw
    throw Error('Could not find showdown library');
  }

}(function (showdown) {
  'use strict';
  showdown.extension('codetagseachline', function () {

    console.log('test1'); // This line is printed at the moment this extension file is requested by main script

    return [{
      type: 'listener',
      listeners: {
        'makehtml.codeBlocks.after': function (source) {

          console.log('test2: ' + source); // doesn't work, would at least expect 'test2: ' on console log here

          source = source.replace(/e/g, 'jbsgfjfdhbg');
          return source;
        }
      }
    }];
  });
}));

output

$ node test.js
test1
<p>Test line 1
<code>Code block line 1
Code block line 2
Code block line 3
</code>Test line 5</p>
tivie commented 2 years ago

I think you're forgetting to register the extension. There's an extension boilerplate you can use.

Regardless, I tested your extension code in jsfiddle and it works https://jsfiddle.net/eyjpzxr0/

tivie commented 2 years ago

Our documentation really needs to be improved

ZTiKnl commented 2 years ago

I'm really sorry, but I'm probably just having a breakdown or something..

I don't see the same as you do... Tried 3 different browsers, but the fiddle doesnt work in any

No errors, true but no text replacement either no console output

If I hit run, I see

<p>foo</p>
<pre><code>Memory
second line
third line
</code></pre>
<p>bar</p>

What I would expect with that fiddle:

<p>foo</p>
<pre><code>jbsgfjfdhbg
second line
third line
</code></pre>
<p>bar</p>

Are we still talking about the same thing here?

Our documentation really needs to be improved

Hehe, Work In Progress, one more thing on the todo list ;)

ZTiKnl commented 2 years ago

Oh and I really do appreciate you taking the time to reply and test, but don't feel obligated to help me, as I'm probably making some silly beginner mistake or something like that

tivie commented 2 years ago

sorry. I'm the one with the mental breakdown. I just finished a 24 hour shift in the ER and I'm not thinking straight! I was using the dev build (in my machine) and forgot that it was different.

Yeah, it wasn't working, but should be working now. Check this fiddle: https://jsfiddle.net/eyjpzxr0/2/

ZTiKnl commented 2 years ago

I just finished a 24 hour shift in the ER

I think the technical term is 'absolute legend'!

Not only a real-life hero but fixing my problems on the side... expect a donation coming up soon ;)

ZTiKnl commented 2 years ago

I had some time to do some more work on the original goal.

I am working under the assumption that I should be manipulating the globals.ghCodeBlocks object, as this holds an array with each gh code block in an array like such:

 [
   {
     text: '\n' +
       '```\n' +
       'Sense: Prompt\n' +
       'Sense: Memory\n' +
       'Device: Sense\n' +
       '\n' +
       'Prompt->Color\n' +
       'Prompt->Person\n' +
       'Color-Person\n' +
       'Voice->Person\n' +
       'SFX->Person\n' +
       'Person->Hearing\n' +
       'Person->Vision\n' +
       '```',
     codeblock: '\n\n¨K0K\n\n'
   }
]

I think I should be listening for the event githubCodeBlocks.after, and not the before as the globals.ghCodeBlocks isnt populated before this point.

showdown.extension('codetageachline', function() {
    console.log('extension registered!');
  var myext = {
    type: 'listener',
    listeners: {
      'githubCodeBlocks.after': function(event, text, converter, options, globals) {
        // console.info(globals.ghCodeBlocks);
        let i = 0;
        while (i < globals.ghCodeBlocks.length) {
          globals.ghCodeBlocks[i].text = globals.ghCodeBlocks[i].text.replace(/Memory/g, 'sjhfbsjdhbfsd');
          i++;
        }
        // console.info(globals.ghCodeBlocks); // changes appear here as expected
        return text;
      }
    }
  };
  return [myext];
});

This function does seem to update the globals.ghCodeBlocks, but the changes are not reflected in the final output.
Starting to think I'm trying to manipulate the wrong variable/array by updating globals.ghCodeBlocks.text?

As always, not expecting help, mostly documenting my path to finding a solution, perhaps it helps someone else on a similar path ;)

tivie commented 2 years ago

I think for your use case it might be easer with an output extension. https://jsfiddle.net/30w9m6ef/

showdown.extension('codetageachline', function() {
  return [{
    type: 'output',
    filter: function (text, converter, options) {
      return text.replace(/<pre><code>([\s\S]+?)<\/code><\/pre>/g, function (fullMatch, inCode) {
        // first split by newline, so we have an array of code lines
        var codeLines = inCode.split('\n');

        // pop the last element since it's an empty line
        if (codeLines[codeLines.length - 1] === '') {
          codeLines.pop();
        }

        codeLines = codeLines
          // then loop through the array of lines of code and wrap it in code tags
          .map(function(line) {
            return '<code>' + line + '</code>';
          })

          // then rejoin the array into a string
          .join('\n');

        // lastly wrap everything in pre tags again
        return '<pre>' + codeLines + '</pre>';
      });
    }
  }];
});
ZTiKnl commented 2 years ago

Can't believe I didn't see this from the start...
Below you'll find the complete working script I modified your example slightly, to also incorporate codeblock classes where needed

CSS:

pre {
    counter-reset: line;
    font-family: Menlo, Monaco, monospace;
    background-color: #333;
    padding: 5px;
    padding-left: 15px;
    color: #CCC;
    border-radius: 3px;
    word-break: keep-all;
    white-space: pre-wrap;
}';
pre code:before {
    content: counter(line);
    counter-increment: line;
    padding-right: 1em;
    width: 1.5em;
    text-align: right;
    opacity: 0.5;
}
code { 
    display: inline;
    background-color:#333;
    color:#CCC;
}

Extension 'codetageachline':

showdown.extension('codetageachline', function() {
  return [{
    type: 'output',
    filter: function (text, converter, options) {
      text = text.replace(/<pre><code>([\s\S]+?)<\/code><\/pre>/g, function (fullMatch, inCode) {
        // first split by newline, so we have an array of code lines
        var codeLines = inCode.split('\n');

        // pop the last element since it's an empty line
        if (codeLines[codeLines.length - 1] === '') {
          codeLines.pop();
        }

        codeLines = codeLines
          // then loop through the array of lines of code and wrap it in code tags
          .map(function(line) {
            return '<code>' + line + '</code>';
          })

          // then rejoin the array into a string
          .join('\n');

        // lastly wrap everything in pre tags again
        return '<pre>' + codeLines + '</pre>';
      });
      text = text.replace(/<pre><code class=\"([\s\S]+?)\">([\s\S]+?)?<\/code><\/pre>/g, function (fullMatch, codeClass, inCode) {
        // first split by newline, so we have an array of code lines
        var codeLines = inCode.split('\n');

        // pop the last element since it's an empty line
        if (codeLines[codeLines.length - 1] === '') {
          codeLines.pop();
        }

        codeLines = codeLines
          // then loop through the array of lines of code and wrap it in code tags
          .map(function(line) {
            return '<code class="' + codeClass + '">' + line + '</code>';
          })

          // then rejoin the array into a string
          .join('\n');

        // lastly wrap everything in pre tags again
        return '<pre>' + codeLines + '</pre>';
      });
      return text;
    }
  }];
});

Yes, there is probably a better/cleaner way to do this, but this is a simple working solution everyone can reuse All credits to @tivie, all I did here was some CSS ;)

ZTiKnl commented 2 years ago

Updated opening post, closing ticket

Thanks again for all the time you spent on my sillyness ;)