showdownjs / ng-showdown

Angular integration for Showdown
BSD 3-Clause "New" or "Revised" License
105 stars 33 forks source link

How to compile an extension's result? #28

Closed ntopulos closed 8 years ago

ntopulos commented 8 years ago

I was able to make a custom extension, but I couldn't find how to compile the contained directives.
The given tag is replaced, but the resulting HTML is not processed by Angular.

How to compile the result with Angular? I don't know if it is even possible to define a compilation from .config(). Should the extension be included somewhere else?

Here is a test extension replacing &test by '<simple-element></simple-element>:

angular.module('myApp')
.config(['$showdownProvider', function($showdownProvider) {
    myExt =  function () {
        return [{
            type: 'output',
            filter: function (text) {
                var regex = /(<p>&amp;test<\/p>)+/g;
                text = text.replace(
                    regex,
                    '<simple-element>extension: ok, directive: NOT</simple-element>'
                );
                return text;
            }
        }];
    };

    $showdownProvider.loadExtension(myExt);   
}]);

The simpleElement directive:

angular.module('myApp')
.directive('simpleElement', [function() {
    return {
        restrict: 'E',
        replace: true,
        template: '<div>Everything works!</div>'
    }
}]);
jspizziri commented 8 years ago

I have a similar problem. I have a showdown extension which is dependent upon scope variables. For instance, in GitHub MD #25 actually refers to https://github.com/showdownjs/ng-showdown/issues/25 (ex #25), although before rendering a link to that url you need to have the context of the URL. Which is only available on the scope level. My idea was to insert a directive as above which would contextually fetch the url and append the id but the HTML isn't compiling.

Perhaps there is a better approach?

jspizziri commented 8 years ago

Here's what I ended up with @ntopulos, hopefully this helps.

I created my own markdown directive as an alternative to the mardown-to-html directive:

// directive/markdown/markdown.js
'use strict';

angular.module('myApp')
  .directive('markdown', function($showdown, $sanitize, $sce) {
    return {
      restrict: 'A',
      link: function (scope, element, attrs) {
        scope.$watch('model', function (newValue) {
          var showdownHTML;
          if (typeof newValue === 'string') {
            showdownHTML = $showdown.makeHtml(newValue);
            scope.trustedHtml = ($showdown.getOption('sanitize')) ? $sanitize(showdownHTML) : $sce.trustAsHtml(showdownHTML);
          } else {
            scope.trustedHtml = typeof newValue;
          }
        });
      },
      scope: {
        model: '=markdown'
      },
      template: '<div bind-html-compile="trustedHtml"></div>'
    };
  });

Please note that this is an exact copy of the markdown-to-html directive with the exception of this: template: '<div bind-html-compile="trustedHtml"></div>'.

With that being done, I created another directive bind-html-compile which compiled the html given to it which looks like so:

// directive/bindHtmlCompile/bindHtmlCompile.js
'use strict';

angular.module('myApp')
  .directive('bindHtmlCompile', function($compile) {
    return {
      restrict: 'A',
      link: function(scope, element, attrs) {
        scope.$watch(function() {
          return scope.$eval(attrs.bindHtmlCompile);
        }, function(value) {
          element.html(value);
          $compile(element.contents())(scope);
        });
      }
    };
  });

Usage:

<!-- instead of this: -->
<div markdown-to-html="myMarkdown"></div>

<!-- use this: -->
<div markdown="myMarkdown"></div>

Voilà!

Additionally, to keep my organization a little cleaner, I created the showdown extensions as angular constants like so:

// constants/myExt.js
(function(){

  'use strict';

  angular.module('myApp')
    .constant('myExt',
      function myExt() {
        var myext1 = {
           type: 'lang',
           regex: /markdown/g,
           replace: 'showdown'
         };
         var myext2 = {
           /* extension code */
         };
         return [myext1, myext2];
      });

})();

And then injected and registered them in the config:

// app.js

  .config(function($showdownProvider, myExt) {
    $showdownProvider.setOption('tables', true);
    //...

    $showdownProvider.loadExtension(myExt);
    //...
  })

Hope this helps!

SyntaxRules commented 8 years ago

Here's a custom video extension I created:

(function () {

  var videoHTML = '<video video-data="someAngularVarriableLocalToThisPage"></video>';

  function video() {
    var replace = function () {
      return videoHTML;
    };
    return [
      // Inline style
      {
        type: 'lang',
        regex: '\\[\\s*?!\\[[^()]*\\([^\\(]*\\s*?\\(.* "video"\\)',
        replace: replace
      }];
  }

  // Client-side export
  if (showdown && showdown.extensions) {
    showdown.extensions.video = video;
  }
})();

To load this into ng-showdown, you can reference it like so. (The way @jspizziri did it works too!)

 angular
    .module('example')
    .config(function ($showdownProvider) {
      $showdownProvider.loadExtension('video');
     });

There a few things I'm doing here:

  1. The extension is actually replacing some markdown text with a custom directive. The directive is <video video-data="someVar"></video>
  2. The extension assumes that someVar already exists on the page that the markdown is running on.

Does this help? I think @jspizziri answered most of the questions, I just wanted to share how I've done it as well.

ntopulos commented 8 years ago

Thank you both for your replies! Unfortunately I wasn't able to make it work, what do I miss?

I've created these jsfiddles, and as you can see the directive is never executed when generated by a ShowDown extension:

In each example I use the extension to replace &test inside the markdown string by <simple-element>extension: executed, directive: NOT executed</simple-element>. Then the SimpleElement directive should replace this by <div>directive executed</div>, but it doesn't.

Edit (works):

Actually @jspizziri's method worked! https://jsfiddle.net/ov0q23Le/1/

I thought it didn't because of a local issue with this line that I forgot to uncomment when creating the jsfiddle: scope.trustedHtml = ($showdown.getOption('sanitize')) ? $sanitize(showdownHTML) : $sce.trustAsHtml(showdownHTML);

tivie commented 8 years ago

@SyntaxRules This thread has very good information. Do you mind creating a wiki entry with this info?

SyntaxRules commented 8 years ago

Done!