From c93483fa55cfff6fb6fe15841603e9a1b4de8d51 Mon Sep 17 00:00:00 2001 From: yisraelx Date: Mon, 2 Jan 2017 00:14:48 +0200 Subject: [PATCH] feat(release): first commit --- .gitignore | 17 ++ .travis.yml | 25 +++ LICENSE | 9 + README.md | 151 ++++++++++++++ demo/README.md | 20 ++ demo/src/app/app.component.css | 33 +++ demo/src/app/app.component.html | 40 ++++ demo/src/app/app.component.ts | 43 ++++ demo/src/app/app.module.ts | 21 ++ demo/src/favicon.ico | Bin 0 -> 5430 bytes demo/src/index.html | 14 ++ demo/src/main.ts | 8 + demo/src/polyfills.ts | 10 + demo/src/styles.css | 0 demo/src/vendor.css | 2 + demo/src/vendor.ts | 14 ++ demo/tsconfig.json | 17 ++ demo/webpack.config.js | 80 ++++++++ karma.conf.js | 77 +++++++ package.json | 131 ++++++++++++ src/base-converter-options.provider.ts | 71 +++++++ src/base-converter.class.ts | 51 +++++ src/index.ts | 8 + src/md-converter.provider.ts | 21 ++ src/md.directive.ts | 204 +++++++++++++++++++ src/md.module.ts | 34 ++++ src/md.pipe.ts | 43 ++++ src/src.directive.ts | 53 +++++ src/tsconfig.json | 17 ++ src/utils.ts | 37 ++++ test/base-converter-options.provider.spec.ts | 13 ++ test/base-converter.class.spec.ts | 12 ++ test/index.ts | 22 ++ test/md-converter.provider.spec.ts | 21 ++ test/md.directive.spec.ts | 178 ++++++++++++++++ test/md.pipe.spec.ts | 58 ++++++ test/src.directive.spec.ts | 112 ++++++++++ test/tsconfig.json | 14 ++ test/utils.spec.ts | 28 +++ test/utils.ts | 23 +++ tslint.json | 123 +++++++++++ webpack.config.js | 66 ++++++ 42 files changed, 1921 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 demo/README.md create mode 100644 demo/src/app/app.component.css create mode 100644 demo/src/app/app.component.html create mode 100644 demo/src/app/app.component.ts create mode 100644 demo/src/app/app.module.ts create mode 100644 demo/src/favicon.ico create mode 100644 demo/src/index.html create mode 100644 demo/src/main.ts create mode 100644 demo/src/polyfills.ts create mode 100644 demo/src/styles.css create mode 100644 demo/src/vendor.css create mode 100644 demo/src/vendor.ts create mode 100644 demo/tsconfig.json create mode 100644 demo/webpack.config.js create mode 100644 karma.conf.js create mode 100644 package.json create mode 100644 src/base-converter-options.provider.ts create mode 100644 src/base-converter.class.ts create mode 100644 src/index.ts create mode 100644 src/md-converter.provider.ts create mode 100644 src/md.directive.ts create mode 100644 src/md.module.ts create mode 100644 src/md.pipe.ts create mode 100644 src/src.directive.ts create mode 100644 src/tsconfig.json create mode 100644 src/utils.ts create mode 100644 test/base-converter-options.provider.spec.ts create mode 100644 test/base-converter.class.spec.ts create mode 100644 test/index.ts create mode 100644 test/md-converter.provider.spec.ts create mode 100644 test/md.directive.spec.ts create mode 100644 test/md.pipe.spec.ts create mode 100644 test/src.directive.spec.ts create mode 100644 test/tsconfig.json create mode 100644 test/utils.spec.ts create mode 100644 test/utils.ts create mode 100644 tslint.json create mode 100644 webpack.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0957c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# NodeJs +*.log +npm-debug.log* +node_modules + +# IDE +.idea +.vscode + +# Build +lib +bundles +demo/build +tmp + +# Report +coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c1a2518 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: node_js +sudo: false +node_js: + - '6.6.0' +cache: + directories: + - node_modules +notifications: + email: false +branches: + only: + - master +before_script: + - npm prune +script: + - npm run lint + - npm run test + - npm run coverage:check + - npm run build +after_success: + - npm run coverage:report + - npm run semantic-release +branches: + except: + - /^v\d+\.\d+\.\d+$/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0fd8a34 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright © 2016 Yisrael Eliav + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfb2155 --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# Angular 2 Markdown Module based on [Showdown library](https://github.com/showdownjs/showdown). +[![Travis build](https://travis-ci.org/yisraelx/ng2-md.svg?branch=master)](https://travis-ci.org/yisraelx/ng2-md) +[![Codecov coverage](https://codecov.io/github/yisraelx/ng2-md/coverage.svg?branch=master)](https://codecov.io/github/yisraelx/ng2-md) +[![Version](https://img.shields.io/npm/v/ng2-md.svg)](https://www.npmjs.com/package/ng2-md) +[![MIT License](https://img.shields.io/npm/l/ng2-md.svg)](https://github.com/yisraelx/ng2-md/blob/master/LICENSE) +[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) +[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) + +## Demo +- Online demo - [try](http://yisraelx.github.io/ng2-md) +- Local demo - [source](https://github.com/yisraelx/ng2-md/blob/master/demo) + +## Install +```bash +$ npm install --save ng2-md +# and install peer dependencies +$ npm install --save @angular/core @angular/http showdown +``` + +## Use +#### Setup +```javascript +import { NgModule } from '@angular/core'; +import { MdModule } from 'ng2-md'; + +@NgModule({ + imports: [ MdModule ] +}) +export class AppModule{} +``` +### MdDirective +#### Binding +```javascript +import { IConverterOptions } from 'ng2-md'; +// ... + text: string = ` + # h1 + ## h2 + `; + options: IConverterOptions = {...} +// ... +``` +```html + +``` +```html + +``` +```html +
+``` +#### Content +```html + + # H1 + ## H2 + +``` +```html + + # H1 + ## H2 + +``` +Note: _there is a problem in content unescaped "{" and "}" (use html code)._ + +### Options +```javascript +import { IConverterOptions } from 'ng2-md'; +// ... + options:IConverterOptions = {...}; +//... +``` +```html +# abc +``` +```html +# abc +``` +### Trim each line +```html + # abc //

abc

+``` +```html +\t# abc\t //

abc

+``` +both tab and space +```html +\t # abc\t //

abc

+``` +### Load .md file (SrcDirective) +```html + +``` +```html + +``` + +### Pipe +```javascript +import { IConverterOptions } from 'ng2-md'; +// ... + text: string = ` + # h1 + ## h2 + `; + options: IConverterOptions = {...} +// ... +``` +```html +{{ text | md }} +``` +```html +{{ text | md:options }} +``` + +### Provider +```javascript +import { MdConverter } from 'ng2-md'; + +class Some{ + constructor(mdConverter: MdConverter){ + console.log(mdConverter.makeHtml("...")); + } +} +``` + +### Default converter options +the default options is the showdown default options +```javascript +import { NgModel } from '@angular/core'; +import { ConverterOptions, BaseConverterOptions } from 'ng2-md'; +export class MyConverterOptions extends ConverterOptions{ + constructor(){ + super({...}); + } +} +@NgModel({ + providers:[ + {provide: ConverterOptions, useClass: MyConverterOptions}, + ] +}) +export class AppModule{} +``` + +## Credits +This library based on [Showdown library](https://github.com/showdownjs/showdown) + +## License +Copyright © 2016 [Yisrael Eliav](https://github.com/yisraelx), +Licensed under the [MIT license](https://github.com/yisraelx/ng2-md/blob/master/LICENSE). diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..bc36aa1 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,20 @@ +### Clone +```bash +$ git clone https://github.com/yisraelx/ng2-md.git +``` +### Install +```bash +$ npm install +``` +### Run +```bash +$ npm run demo +# or +$ ../node_modules/.bin/webpack-dev-server +``` +### Build +```bash +$ npm run build:demo +# or +$ ../node_modules/.bin/webpack +``` \ No newline at end of file diff --git a/demo/src/app/app.component.css b/demo/src/app/app.component.css new file mode 100644 index 0000000..b31b1fd --- /dev/null +++ b/demo/src/app/app.component.css @@ -0,0 +1,33 @@ +.app-title { + width: 100%; + text-align: center; +} + +md-sidenav { + width: 30%; + text-align: center; +} + +.page { + height: calc(90%); +} + +.content { + display: flex; + height: 100%; +} + +md, textarea { + height: 100%; + overflow-y: scroll; + border: none; + padding: 10px; +} + +.left, .right { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + padding: 1rem; +} \ No newline at end of file diff --git a/demo/src/app/app.component.html b/demo/src/app/app.component.html new file mode 100644 index 0000000..48999d1 --- /dev/null +++ b/demo/src/app/app.component.html @@ -0,0 +1,40 @@ + + + +
+ {{key}} + + + +
+
+
+
+ +
+
+ + +

{{title}}

+
+
+
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/demo/src/app/app.component.ts b/demo/src/app/app.component.ts new file mode 100644 index 0000000..ce6a252 --- /dev/null +++ b/demo/src/app/app.component.ts @@ -0,0 +1,43 @@ +import { Component } from '@angular/core'; +import { IConverterOptions } from '../../../src'; + +@Component({ + selector: 'my-app', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + + title = 'Angular 2 Markdown Demo!'; + md: string = `## hello markdown! +\`\`\`js +let a = 1; +let b = 2; +let sum = a+b; +console.log(\`sum: \${sum}\`); +\`\`\``; + + options: IConverterOptions = { + omitExtraWLInCodeBlocks: true, + noHeaderId: true, + prefixHeaderId: true, + parseImgDimensions: true, + headerLevelStart: 1, + literalMidWordUnderscores: true, + strikethrough: true, + tables: true, + tablesHeaderId: true, + ghCodeBlocks: true, + tasklists: true, + smoothLivePreview: true, + trimEachLine: 'space' + }; + + keys(obj: Object) { + return Object.keys(obj); + } + + isType(value: any, type: string) { + return typeof value === type; + } +} diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts new file mode 100644 index 0000000..ff8e84c --- /dev/null +++ b/demo/src/app/app.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { MaterialModule } from '@angular/material'; +import { AppComponent } from './app.component'; +import { MdModule } from '../../../src'; + +@NgModule({ + imports: [ + BrowserModule, + FormsModule, + MaterialModule.forRoot(), + MdModule + ], + declarations: [ + AppComponent + ], + bootstrap: [AppComponent] +}) +export class AppModule { +} \ No newline at end of file diff --git a/demo/src/favicon.ico b/demo/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8081c7ceaf2be08bf59010158c586170d9d2d517 GIT binary patch literal 5430 zcmc(je{54#6vvCoAI3i*G5%$U7!sA3wtMZ$fH6V9C`=eXGJb@R1%(I_{vnZtpD{6n z5Pl{DmxzBDbrB>}`90e12m8T*36WoeDLA&SD_hw{H^wM!cl_RWcVA!I+x87ee975; z@4kD^=bYPn&pmG@(+JZ`rqQEKxW<}RzhW}I!|ulN=fmjVi@x{p$cC`)5$a!)X&U+blKNvN5tg=uLvuLnuqRM;Yc*swiexsoh#XPNu{9F#c`G zQLe{yWA(Y6(;>y|-efAy11k<09(@Oo1B2@0`PtZSkqK&${ zgEY}`W@t{%?9u5rF?}Y7OL{338l*JY#P!%MVQY@oqnItpZ}?s z!r?*kwuR{A@jg2Chlf0^{q*>8n5Ir~YWf*wmsh7B5&EpHfd5@xVaj&gqsdui^spyL zB|kUoblGoO7G(MuKTfa9?pGH0@QP^b#!lM1yHWLh*2iq#`C1TdrnO-d#?Oh@XV2HK zKA{`eo{--^K&MW66Lgsktfvn#cCAc*(}qsfhrvOjMGLE?`dHVipu1J3Kgr%g?cNa8 z)pkmC8DGH~fG+dlrp(5^-QBeEvkOvv#q7MBVLtm2oD^$lJZx--_=K&Ttd=-krx(Bb zcEoKJda@S!%%@`P-##$>*u%T*mh+QjV@)Qa=Mk1?#zLk+M4tIt%}wagT{5J%!tXAE;r{@=bb%nNVxvI+C+$t?!VJ@0d@HIyMJTI{vEw0Ul ze(ha!e&qANbTL1ZneNl45t=#Ot??C0MHjjgY8%*mGisN|S6%g3;Hlx#fMNcL<87MW zZ>6moo1YD?P!fJ#Jb(4)_cc50X5n0KoDYfdPoL^iV`k&o{LPyaoqMqk92wVM#_O0l z09$(A-D+gVIlq4TA&{1T@BsUH`Bm=r#l$Z51J-U&F32+hfUP-iLo=jg7Xmy+WLq6_tWv&`wDlz#`&)Jp~iQf zZP)tu>}pIIJKuw+$&t}GQuqMd%Z>0?t%&BM&Wo^4P^Y z)c6h^f2R>X8*}q|bblAF?@;%?2>$y+cMQbN{X$)^R>vtNq_5AB|0N5U*d^T?X9{xQnJYeU{ zoZL#obI;~Pp95f1`%X3D$Mh*4^?O?IT~7HqlWguezmg?Ybq|7>qQ(@pPHbE9V?f|( z+0xo!#m@Np9PljsyxBY-UA*{U*la#8Wz2sO|48_-5t8%_!n?S$zlGe+NA%?vmxjS- zHE5O3ZarU=X}$7>;Okp(UWXJxI%G_J-@IH;%5#Rt$(WUX?6*Ux!IRd$dLP6+SmPn= z8zjm4jGjN772R{FGkXwcNv8GBcZI#@Y2m{RNF_w8(Z%^A*!bS*!}s6sh*NnURytky humW;*g7R+&|Ledvc- + + + + Angular 2 Markdown Demo! + + + + + + +Loading... + + \ No newline at end of file diff --git a/demo/src/main.ts b/demo/src/main.ts new file mode 100644 index 0000000..c72c808 --- /dev/null +++ b/demo/src/main.ts @@ -0,0 +1,8 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { enableProdMode } from '@angular/core'; +import { AppModule } from './app/app.module'; +import './styles.css'; +if (process.env.ENV === 'production') { + enableProdMode(); +} +platformBrowserDynamic().bootstrapModule(AppModule); \ No newline at end of file diff --git a/demo/src/polyfills.ts b/demo/src/polyfills.ts new file mode 100644 index 0000000..5137c23 --- /dev/null +++ b/demo/src/polyfills.ts @@ -0,0 +1,10 @@ +import 'core-js/es6'; +import 'core-js/es7/reflect'; +require('zone.js/dist/zone'); +if (process.env.ENV === 'production') { + // Production +} else { + // Development + Error['stackTraceLimit'] = Infinity; + require('zone.js/dist/long-stack-trace-zone'); +} \ No newline at end of file diff --git a/demo/src/styles.css b/demo/src/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/demo/src/vendor.css b/demo/src/vendor.css new file mode 100644 index 0000000..a0c342c --- /dev/null +++ b/demo/src/vendor.css @@ -0,0 +1,2 @@ +@import '~https://fonts.googleapis.com/icon?family=Material+Icons'; +@import '~@angular/material/core/theming/prebuilt/deeppurple-amber.css'; \ No newline at end of file diff --git a/demo/src/vendor.ts b/demo/src/vendor.ts new file mode 100644 index 0000000..47946b9 --- /dev/null +++ b/demo/src/vendor.ts @@ -0,0 +1,14 @@ +// Angular +import '@angular/platform-browser'; +import '@angular/platform-browser-dynamic'; +import '@angular/core'; +import '@angular/router'; +import '@angular/http'; +import '@angular/material'; +// RxJS +import 'rxjs'; +// Other vendors for example jQuery, Lodash or Bootstrap +// You can import js, ts, css, sass, .. +import 'hammerjs'; + +import './vendor.css'; \ No newline at end of file diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000..29a4c24 --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "removeComments": false, + "noImplicitAny": true, + "suppressImplicitAnyIndexErrors": true, + "types":[ + "core-js", + "node" + ] + } +} \ No newline at end of file diff --git a/demo/webpack.config.js b/demo/webpack.config.js new file mode 100644 index 0000000..4f6addf --- /dev/null +++ b/demo/webpack.config.js @@ -0,0 +1,80 @@ +var webpack = require('webpack'), + CleanWebpackPlugin = require('clean-webpack-plugin'), + HtmlWebpackPlugin = require('html-webpack-plugin'), + ExtractTextPlugin = require('extract-text-webpack-plugin'), + path = require('path'); + +module.exports = { + entry: { + 'polyfills': root('src', 'polyfills.ts'), + 'vendor': root('src', 'vendor.ts'), + 'app': root('src', 'main.ts') + }, + + output: { + path: root('build'), + filename: '[name].js', + chunkFilename: '[id].chunk.js' + }, + + resolve: { + extensions: ['', '.ts', '.js'] + }, + + devServer: { + historyApiFallback: true, + stats: 'minimal', + open: true, + port: 8080, + inline: true + }, + + module: { + loaders: [ + { + test: /\.ts$/, + loaders: ['ts-loader', 'angular2-template-loader'] + }, + { + test: /\.html$/, + loader: 'html' + }, + { + test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico|md)$/, + loader: 'file?name=assets/[name].[hash].[ext]' + }, + { + test: /\.css$/, + exclude: root('src', 'app'), + loader: ExtractTextPlugin.extract('style', 'css?sourceMap') + }, + { + test: /\.css$/, + include: root('src', 'app'), + loader: 'raw' + } + ] + }, + + plugins: [ + new CleanWebpackPlugin('build'), + new webpack.optimize.CommonsChunkPlugin({ + name: ['app', 'vendor', 'polyfills'] + }), + + new HtmlWebpackPlugin({ + template: root('src', 'index.html'), + favicon: root('src', 'favicon.ico') + }), + + new ExtractTextPlugin('[name].css') + ], + + devtool: 'source-map' +}; + + + +function root(...args) { + return path.join.apply(path, [__dirname].concat(args)) +} \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..9f75be1 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,77 @@ +var webpack = require('webpack'), + CleanWebpackPlugin = require('clean-webpack-plugin'); + +module.exports = function (config) { + config.set({ + basePath: '', + + frameworks: ['jasmine','source-map-support'], + + files: [ + { pattern: './test/index.ts', watched: false } + ], + + preprocessors: { + './test/index.ts': ['webpack'] + }, + + webpack: { + devtool: 'inline-source-map', + + resolve: { + extensions: ['', '.ts', '.js'], + modulesDirectories: ['node_modules', 'src'] + }, + + module: { + loaders: [ + { + test: /\.ts$/, + loader: 'ts-loader' + } + ], + postLoaders: [ + { + test: /\.ts$/, + loader: 'istanbul-instrumenter-loader', + exclude: [ + /node_modules/, + /test/ + ] + } + ] + }, + + plugins:[ + new CleanWebpackPlugin('./coverage') + ] + }, + + webpackMiddleware: { + stats: 'errors-only' + }, + + webpackServer: { + noInfo: true + }, + + coverageReporter: { + type: 'in-memory' + }, + + remapCoverageReporter: { + 'text-summary': null, + html: './coverage/html', + json: './coverage/coverage-final.json', + lcovonly: './coverage/lcov.info' + }, + + reporters: ['progress', 'coverage','remap-coverage'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: false, + browsers: ['PhantomJS'], + singleRun: true + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..538534a --- /dev/null +++ b/package.json @@ -0,0 +1,131 @@ +{ + "name": "ng2-md", + "description": "Angular 2 Markdown Module.", + "version": "0.0.0-development", + "license": "MIT", + "author": { + "name": "Yisrael Eliav", + "email": "yisraelx@gmail.com", + "url": "https://github.com/yisraelx/" + }, + "main": "lib/index.js", + "browser": "bundles/ng-md.umd.js", + "typings": "lib/index.d.ts", + "files": [ + "bundles", + "lib", + "LICENSE", + "README.md" + ], + "keywords": [ + "ng2", + "ng2-markdown", + "ng2-showdown", + "angular2", + "angular2-md", + "angular2-markdown", + "angular2-showdown", + "markdown", + "md", + "showdown" + ], + "scripts": { + "build": "run-s build:*", + "build:lib": "tsc --project src", + "build:bundles": "webpack --progress --profile --bail", + "build:demo": "webpack --config demo/webpack.config.js --progress --profile --bail", + "coverage:check": "istanbul check-coverage --statements 80 --branches 80 --functions 80 --lines 80", + "coverage:report": "codecov -f coverage/*.json", + "clean": "rimraf lib bundles demo/build coverage", + "commit": "git-cz", + "demo": "webpack-dev-server --config demo/webpack.config.js --progress", + "deploy:demo": "gh-pages -d demo/build", + "lint": "run-s lint:*", + "lint:demo": "tslint demo/src/**/*.ts", + "lint:lib": "tslint src/**/*.ts", + "lint:test": "tslint test/**/*.ts", + "precommit": "run-s lint test coverage:check", + "start": "run-s demo", + "test": "karma start", + "watch:lib": "npm run build:lib -- --watch", + "watch:test": "npm test -- --auto-watch --no-single-run", + "semantic-release": "semantic-release pre && npm publish && semantic-release post" + }, + "repository": { + "type": "git", + "url": "https://github.com/yisraelx/ng2-md.git" + }, + "bugs": { + "url": "https://github.com/yisraelx/ng2-md/issues" + }, + "homepage": "http://yisraelx.github.io/ng2-md", + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } + }, + "engines": { + "node": ">= 5.4.1 < 7" + }, + "peerDependencies": { + "@angular/core": "^2.4.1", + "@angular/http": "^2.4.1", + "showdown": "^1.5.5" + }, + "devDependencies": { + "@angular/common": "^2.4.1", + "@angular/compiler": "^2.4.1", + "@angular/core": "^2.4.1", + "@angular/forms": "^2.4.1", + "@angular/http": "^2.4.1", + "@angular/material": "^2.0.0-beta.1", + "@angular/platform-browser": "^2.4.1", + "@angular/platform-browser-dynamic": "^2.4.1", + "@angular/router": "^3.4.1", + "@types/core-js": "^0.9.35", + "@types/jasmine": "^2.5.40", + "@types/node": "^6.0.55", + "@types/showdown": "^1.4.31", + "@types/webpack": "^2.1.0", + "angular2-template-loader": "^0.6.0", + "clean-webpack-plugin": "^0.1.14", + "codecov": "^1.0.1", + "codelyzer": "^2.0.0-beta.4", + "commitizen": "^2.9.3", + "core-js": "^2.4.1", + "css-loader": "^0.26.1", + "cz-conventional-changelog": "^1.2.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.9.0", + "gh-pages": "^0.12.0", + "hammerjs": "^2.0.8", + "html-loader": "^0.4.4", + "html-webpack-plugin": "^2.25.0", + "husky": "^0.12.0", + "istanbul": "^0.4.5", + "istanbul-instrumenter-loader": "^1.2.0", + "jasmine-core": "^2.5.2", + "karma": "^1.3.0", + "karma-coverage": "^1.1.1", + "karma-jasmine": "^1.1.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-remap-coverage": "^0.1.4", + "karma-source-map-support": "^1.2.0", + "karma-webpack": "^1.8.1", + "npm-run-all": "^4.0.0", + "null-loader": "^0.1.1", + "phantomjs-prebuilt": "^2.1.14", + "raw-loader": "^0.5.1", + "rimraf": "^2.5.4", + "rxjs": "^5.0.2", + "semantic-release": "^6.3.2", + "showdown": "^1.5.5", + "style-loader": "^0.13.1", + "ts-loader": "^1.3.3", + "tslint": "^4.2.0", + "typescript": "^2.1.4", + "webpack": "^1.14.0", + "webpack-dev-server": "^1.16.2", + "zone.js": "^0.7.4" + } +} diff --git a/src/base-converter-options.provider.ts b/src/base-converter-options.provider.ts new file mode 100644 index 0000000..9e27eb7 --- /dev/null +++ b/src/base-converter-options.provider.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import $ from './utils'; +import { IConverterConstructorOptions } from './base-converter.class'; + +export class ConverterOptions { + + omitExtraWLInCodeBlocks: boolean; + noHeaderId: boolean; + prefixHeaderId: string | boolean; + parseImgDimensions: boolean; + headerLevelStart: number; + literalMidWordUnderscores: boolean; + strikethrough: boolean; + tables: boolean; + tablesHeaderId: boolean; + ghCodeBlocks: boolean; + tasklists: boolean; + smoothLivePreview: boolean; + trimEachLine: boolean | 'space' | 'tab'; + extensions: string | string[]; + + constructor(options?: IConverterConstructorOptions) { + this.merge(options); + } + + public merge(options: IConverterConstructorOptions) { + if (!$.isObject(options)) return; + $.forIn(options, (val, key) => { + this[key] = val; + }); + } +} + +/** + * @example + * ```javascript + * import { NgModel } from '@angular/core'; + * import { ConverterOptions, BaseConverterOptions } from 'ng2-md'; + * export class MyConverterOptions extends ConverterOptions{ + * constructor(){ + * super({...}); + * } + * } + * @NgModel({ + * providers:[ + * {provide: ConverterOptions, useClass: MyConverterOptions}, + * ] + * }) + * export class AppModule{} + * ``` + */ +@Injectable() +export class BaseConverterOptions extends ConverterOptions { + constructor() { + super({ + omitExtraWLInCodeBlocks: false, + noHeaderId: false, + prefixHeaderId: false, + parseImgDimensions: false, + headerLevelStart: 1, + literalMidWordUnderscores: false, + strikethrough: false, + tables: false, + tablesHeaderId: false, + ghCodeBlocks: true, + tasklists: false, + smoothLivePreview: false, + trimEachLine: false + }); + } +} \ No newline at end of file diff --git a/src/base-converter.class.ts b/src/base-converter.class.ts new file mode 100644 index 0000000..a8b9c14 --- /dev/null +++ b/src/base-converter.class.ts @@ -0,0 +1,51 @@ +import { Converter } from 'showdown'; +import $ from './utils'; +import { ConverterOptions } from './base-converter-options.provider'; + +export interface IConverterOptions { + omitExtraWLInCodeBlocks?: boolean; + noHeaderId?: boolean; + prefixHeaderId?: string | boolean; + parseImgDimensions?: boolean; + headerLevelStart?: number; + literalMidWordUnderscores?: boolean; + strikethrough?: boolean; + tables?: boolean; + tablesHeaderId?: boolean; + ghCodeBlocks?: boolean; + tasklists?: boolean; + smoothLivePreview?: boolean; + trimEachLine?: boolean | 'tab' | 'space'; +} + +export interface IConverterConstructorOptions extends IConverterOptions { + extensions?: string | string[]; +} + +export class BaseConverter extends Converter { + + constructor(options?: IConverterConstructorOptions | ConverterOptions) { + super(options); + // override makeHtml method (define in super constructor) + let {makeHtml} = this; + this.makeHtml = (text: string): string => { + text = this._preMakeHtml(text); + return makeHtml.call(this, text); + }; + } + + public setOptions(options: IConverterOptions): void { + if ($.isObject(options)) { + $.forIn(options, (value: any, optionKey: string) => { + this.setOption(optionKey, value); + }); + } + } + + /** pre super.makeHtml (situation that not possible to achieve it with subParsers or extensions) */ + private _preMakeHtml(text: string): string { + let {trimEachLine} = this.getOptions() as IConverterOptions; + text = $.trimEachLine(text, trimEachLine); + return text; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..abdf489 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,8 @@ +export { BaseConverter, IConverterConstructorOptions, IConverterOptions } from './base-converter.class'; +export { ConverterOptions, BaseConverterOptions } from './base-converter-options.provider'; +export { MdConverter } from './md-converter.provider'; +export { MdDirective } from './md.directive'; +export { SrcDirective } from './src.directive'; +export { MdPipe } from './md.pipe'; + +export { MdModule } from './md.module'; \ No newline at end of file diff --git a/src/md-converter.provider.ts b/src/md-converter.provider.ts new file mode 100644 index 0000000..eda6add --- /dev/null +++ b/src/md-converter.provider.ts @@ -0,0 +1,21 @@ +import { Injectable, Optional } from '@angular/core'; +import { ConverterOptions } from './base-converter-options.provider'; +import { BaseConverter } from './base-converter.class'; + +/** + * @example + * ```javascript + * import { MdConverter } from 'ng2-md'; + * class Some{ + * constructor(mdConverter: MdConverter){ + * console.log(mdConverter.makeHtml("...")); + * } + * } + * ``` + */ +@Injectable() +export class MdConverter extends BaseConverter { + constructor(@Optional() options?: ConverterOptions) { + super(options); + } +} \ No newline at end of file diff --git a/src/md.directive.ts b/src/md.directive.ts new file mode 100644 index 0000000..3e12800 --- /dev/null +++ b/src/md.directive.ts @@ -0,0 +1,204 @@ +import { Directive, ElementRef, Input, OnInit, Optional } from '@angular/core'; +import $ from './utils'; +import { ConverterOptions } from './base-converter-options.provider'; +import { BaseConverter, IConverterOptions } from './base-converter.class'; + +let optionsProperties: string[] = [ + 'omitExtraWLInCodeBlocks', 'noHeaderId', 'prefixHeaderId', 'parseImgDimensions', 'headerLevelStart', 'literalMidWordUnderscores', 'strikethrough', 'tables', 'tablesHeaderId', 'ghCodeBlocks', 'tasklists', 'smoothLivePreview', 'trimEachLine' +]; + +export enum MD_COMPONENT_TYPES { + NONE, + SRC, + BINDING, + CONTENT +} + +export enum MD_COMPONENT_STATUSES { + CREATED, + INIT, + PROCESSING, + READY, +} + +/** + * @problem in content use {} - [unescaped "{":](https://github.com/angular/angular/issues/11859) the solution is to sanitize (html char code etc.). + * + * @example + * ```javascript + * import { NgModule } from '@angular/core'; + * import { MdDirective } from 'ng2-md'; + * @NgModule({ + * declarations: [ MdDirective ]; + * }) + * export class AppModule{} + * ``` + * ```javascript + * import { IConverterOptions } from 'ng2-md'; + * // ... + * text: string = "..."; + * options: IConverterOptions = {...}; + * // ... + * ``` + * ```html + * + * ``` + * ```html + *
+ * ``` + * ```html + * + * ``` + * ```html + * + * ``` + * ```html + * # abc //

abc

+ * ``` + * ```html + * \t# abc\t //

abc

+ * ``` + * both tab and space + * ```html + * \t # abc\t //

abc

+ * ``` + */ +@Directive({ + selector: 'md,[md]', + inputs: [].concat(optionsProperties) +}) +export class MdDirective extends BaseConverter implements OnInit { + + public static readonly TYPES = MD_COMPONENT_TYPES; + public static readonly STATUSES = MD_COMPONENT_STATUSES; + + // options getter setter dynamic definition (the code after the class) + public omitExtraWLInCodeBlocks: boolean; + public noHeaderId: boolean; + public prefixHeaderId: string | boolean; + public parseImgDimensions: boolean; + public headerLevelStart: number; + public literalMidWordUnderscores: boolean; + public strikethrough: boolean; + public tables: boolean; + public tablesHeaderId: boolean; + public ghCodeBlocks: boolean; + public tasklists: boolean; + public smoothLivePreview: boolean; + public trimEachLine: boolean | 'tab' | 'space'; + + private _value: string; + private _type: number = MdDirective.TYPES.NONE; + private _status: number = MdDirective.STATUSES.CREATED; + + /** Value of the component (the input md text pre converter). */ + @Input() + public get value(): string { + return this._value; + } + + public set value(value: string) { + this.setValue(value); + } + + /** Type of the input source [binding, content, src]. */ + public get type(): string { + return MdDirective.TYPES[this._type].toLowerCase(); + } + + /** Status of the component life cycle. */ + public get status(): string { + return MdDirective.STATUSES[this._status].toLowerCase(); + } + + /** Alias to value */ + @Input() + public get md(): string { + return this.value; + } + + public set md(value: string) { + this.value = value; + } + + @Input() + public get options(): IConverterOptions { + return this.getOptions(); + } + + public set options(options: IConverterOptions) { + this.setOptions(options); + } + + constructor(private _elementRef: ElementRef, @Optional() options?: ConverterOptions) { + super(options); + + // override the setOption method (define in the super constructor) + this.setOption = (optionKey: string, value: any) => { + this.getOptions()[optionKey] = value; + this._onChange(); + }; + } + + public ngOnInit(): void { + + if (this._type === MdDirective.TYPES.NONE && !$.isEmpty(this._elementRef.nativeElement.innerText)) { + let value = this._elementRef.nativeElement.innerHTML; + this.setValue(value, MdDirective.TYPES.CONTENT); + } + + if (this._status === MdDirective.STATUSES.CREATED) { + this._status = MdDirective.STATUSES.INIT; + } + + } + + public setValue(value: string, type: number = MdDirective.TYPES.BINDING): void { + this._value = value; + this._type = type; + this._onChange(); + } + + public setOptions(options: IConverterOptions): void { + super.setOptions(options); + this._onChange(); + } + + public compile(): void { + if (this._type === MdDirective.TYPES.NONE) return; + this._status = MdDirective.STATUSES.PROCESSING; + this._elementRef.nativeElement.innerHTML = this.toHTML(); + this._status = MdDirective.STATUSES.READY; + } + + public registerOnChange(fn: (() => void)): void { + if (!$.isFunction(fn)) throw new Error('Arg fn is missing or invalid.'); + this._onChange = fn; + } + + /** Converter the component (md value) to html */ + public toHTML(): string { + let {_value: value} = this; + return this.makeHtml(value); + } + + /** Default OnChange method, Called on change value or options */ + private _onChange: (() => void) = (): void => { + this.compile(); + } + +} + +// define options properties getter setter for angular directive and direct access +optionsProperties.forEach((key: string) => { + Object.defineProperty(MdDirective.prototype, key, { + set(value: any): void { + this.setOption(key, $.isEmpty(value) ? true : value); + }, + get(): any { + return this.getOption(key); + }, + enumerable: true, + configurable: true + }); +}); \ No newline at end of file diff --git a/src/md.module.ts b/src/md.module.ts new file mode 100644 index 0000000..26f0f8d --- /dev/null +++ b/src/md.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { MdDirective } from './md.directive'; +import { SrcDirective } from './src.directive'; +import { MdPipe } from './md.pipe'; +import { MdConverter } from './md-converter.provider'; +import { ConverterOptions, BaseConverterOptions } from './base-converter-options.provider'; + + +let declarations = [ + MdDirective, + MdPipe, + SrcDirective +]; + +/** + * @example + * ```javascript + * import { NgModule } from '@angular/core'; + * import { MdModule} from 'ng2-md'; + * @NgModule({ + * imports: [ MdModule ]; + * }) + * export class AppModule{} + */ +@NgModule({ + declarations, + providers: [ + MdConverter, + {provide: ConverterOptions, useClass: BaseConverterOptions} + ], + exports: declarations +}) +export class MdModule { +} \ No newline at end of file diff --git a/src/md.pipe.ts b/src/md.pipe.ts new file mode 100644 index 0000000..e711860 --- /dev/null +++ b/src/md.pipe.ts @@ -0,0 +1,43 @@ +import { PipeTransform, Pipe, Optional } from '@angular/core'; +import { ConverterOptions } from './base-converter-options.provider'; +import { BaseConverter, IConverterOptions } from './base-converter.class'; + +/** + * @example + * ```javascript + * import { NgModule } from '@angular/core'; + * import { MdPipe } from 'ng2-md'; + * @NgModule({ + * declarations: [ MdPipe ]; + * }) + * export class AppModule{} + * ``` + * ```javascript + * import { IConverterOptions } from 'ng2-md'; + * // ... + * md: string = "..."; + * options: IConverterOptions = {...}; + * // ... + * ``` + * ```html + * {{ md | md }} + * ``` + * ```html + * {{ md | md:options}} + * ``` + */ +@Pipe({ + name: 'md', + pure: false +}) +export class MdPipe extends BaseConverter implements PipeTransform { + + constructor(@Optional() options: ConverterOptions) { + super(options); + } + + transform(md: string = '', options?: IConverterOptions): string { + this.setOptions(options); + return this.makeHtml(md); + } +} \ No newline at end of file diff --git a/src/src.directive.ts b/src/src.directive.ts new file mode 100644 index 0000000..a2d55c4 --- /dev/null +++ b/src/src.directive.ts @@ -0,0 +1,53 @@ +import { Directive, Input } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { MdDirective } from './md.directive'; + +/** + * @example + * ```javascript + * import { NgModule } from '@angular/core'; + * import { MdDirective, SrcDirective } from 'ng2-md'; + * @NgModule({ + * declarations: [ MdDirective, SrcDirective ]; + * }) + * export class AppModule{} + * ``` + * ```html + * + * ``` + * ```html + * + * ``` + * ```html + *
+ * ``` + */ +@Directive({ + selector: 'md[src],[md][src]' +}) +export class SrcDirective { + + private _src: string; + + /** Source of md file */ + @Input() + public get src(): string { + return this._src; + } + + public set src(src: string) { + this._src = src; + this.load(); + } + + constructor(private _mdDirective: MdDirective, private _http: Http) { + } + + public load(): void { + let {src} = this; + this._http.get(src).subscribe((res: Response) => { + let value = res.text(); + this._mdDirective.setValue(value, MdDirective.TYPES.SRC); + }); + } +} \ No newline at end of file diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..db44070 --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "outDir": "../lib", + "rootDir": ".", + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "removeComments": false, + "sourceMap": true + }, + "files": [ + "index.ts" + ] +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e9786c3 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,37 @@ +export default class Utils { + + static isEmpty(val: any): boolean { + return val === void 0 || val === null || `${val}`.trim() === ''; + } + + static isObject(obj: any): boolean { + return typeof obj === 'object'; + } + + static isFunction(fn: any): boolean { + return typeof fn === 'function'; + } + + static forIn(object: { [key: string]: any } = {}, cb: (val: any, key: string, object: { [key: string]: any }) => void = (() => { + })): void { + Object.keys(object).forEach((key: string) => { + cb(object[key], key, object); + }); + } + + static trimEachLine(text: string, trimEachLine?: boolean | 'tab' | 'space') { + if (this.isEmpty(text)) return text; + + switch (trimEachLine) { + case 'space': + return text.replace(/^(?=\n)$|^[ ]+|[ ]+$/gm, ''); + case 'tab': + return text.replace(/^(?=\n)$|^\t+|\t+$/gm, ''); + case true: + return text.replace(/^(?=\n)$|^\s+|\s+$/gm, ''); + default: + return text; + } + + } +} \ No newline at end of file diff --git a/test/base-converter-options.provider.spec.ts b/test/base-converter-options.provider.spec.ts new file mode 100644 index 0000000..9f0643d --- /dev/null +++ b/test/base-converter-options.provider.spec.ts @@ -0,0 +1,13 @@ +import { ConverterOptions } from '../src/base-converter-options.provider'; + +describe('BaseConverterOptions', () => { + + it('should be set options', () => { + let converterOptions = new ConverterOptions(); + converterOptions.merge({ noHeaderId: false, tables: true }); + + expect(converterOptions.tables).toBeTruthy(); + expect(converterOptions.noHeaderId).toBeFalsy(); + }); + +}); \ No newline at end of file diff --git a/test/base-converter.class.spec.ts b/test/base-converter.class.spec.ts new file mode 100644 index 0000000..032667e --- /dev/null +++ b/test/base-converter.class.spec.ts @@ -0,0 +1,12 @@ +import { BaseConverter } from '../src/base-converter.class'; + +describe('BaseConverter', () => { + + it('should be set mulitple options', () => { + let baseConverter = new BaseConverter(); + baseConverter.setOptions({ noHeaderId: false, tables: true }); + expect(baseConverter.getOption('tables')).toBeTruthy(); + expect(baseConverter.getOptions().noHeaderId).toBeFalsy(); + }); + +}); \ No newline at end of file diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..d5d2390 --- /dev/null +++ b/test/index.ts @@ -0,0 +1,22 @@ +Error.stackTraceLimit = Infinity; + +import 'core-js/es6'; +import 'core-js/es7/reflect'; + +import 'zone.js/dist/zone'; +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/proxy'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; + +import { TestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + + +let testsContext = (require as any).context('.', true, /\.spec\.ts/); +testsContext.keys().forEach(testsContext); + + +TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); \ No newline at end of file diff --git a/test/md-converter.provider.spec.ts b/test/md-converter.provider.spec.ts new file mode 100644 index 0000000..32359f8 --- /dev/null +++ b/test/md-converter.provider.spec.ts @@ -0,0 +1,21 @@ +import { TestBed, TestModuleMetadata } from '@angular/core/testing'; +import { ConverterOptions, BaseConverterOptions } from '../src/base-converter-options.provider'; +import { MdConverter } from '../src/md-converter.provider'; + +let mdProviderModuleMetadata: TestModuleMetadata = { + providers: [ + {provide: ConverterOptions, useClass: BaseConverterOptions}, + MdConverter + ] +}; + +describe('MdConverter', () => { + + it('should be converted md to html', () => { + let fixture = TestBed.configureTestingModule(mdProviderModuleMetadata); + let mdConverter: MdConverter = fixture.get(MdConverter); + + expect(mdConverter.makeHtml('# abc')).toBe('

abc

'); + }); + +}); \ No newline at end of file diff --git a/test/md.directive.spec.ts b/test/md.directive.spec.ts new file mode 100644 index 0000000..410edb7 --- /dev/null +++ b/test/md.directive.spec.ts @@ -0,0 +1,178 @@ +import { TestModuleMetadata, ComponentFixtureAutoDetect } from '@angular/core/testing'; +import { MdDirective } from '../src/md.directive'; +import { ConverterOptions, BaseConverterOptions } from '../src/base-converter-options.provider'; +import $ from './utils'; + +let mdDirectiveModuleMetadata: TestModuleMetadata = { + declarations: [MdDirective], + providers: [ + { provide: ConverterOptions, useClass: BaseConverterOptions }, + { provide: ComponentFixtureAutoDetect, useValue: true } + ] +}; + +describe('MdDirective', () => { + + it('should be the mdDirective.type to be equal to MdDirective.TYPES.NONE', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' } + }); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + + expect(mdDirective.type).toBe(MdDirective.TYPES[MdDirective.TYPES.NONE].toLowerCase()); + expect(mdDirective.status).toBe(MdDirective.STATUSES[MdDirective.STATUSES.INIT].toLowerCase()); + }); + + it('should be the mdDirective.type to be equal to MdDirective.TYPES.BINDING', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' }, + scope: { text: '# abc' } + }); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + + expect(mdDirective.type).toBe(MdDirective.TYPES[MdDirective.TYPES.BINDING].toLowerCase()); + expect(mdDirective.status).toBe(MdDirective.STATUSES[MdDirective.STATUSES.READY].toLowerCase()); + }); + + it('should be get set mdDirective.value and alias mdDirective.md', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' } + }); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + + expect(mdDirective.md).toBe(mdDirective.value); + + mdDirective.value = '# abc'; + expect(mdDirective.value).toBe('# abc'); + expect(mdDirective.md).toBe('# abc'); + expect(mdDirective.md).toBe(mdDirective.value); + + mdDirective.md = '## abc'; + expect(mdDirective.value).toBe('## abc'); + expect(mdDirective.md).toBe('## abc'); + expect(mdDirective.md).toBe(mdDirective.value); + }); + + it('should be replace the on change method', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' }, + scope: { text: '# abc' } + }); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + mdDirective.registerOnChange(function () { + this._value = this._value.toUpperCase(); + this.compile(); + }); + (mdDirective as any)._onChange(); + + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

ABC

'); + }); + + it('should be throw error if registerOnChange arg fn is not function', () => { + let mdDirective: MdDirective = new MdDirective({} as any); + let execute = () => { + mdDirective.registerOnChange(void 0); + }; + expect(execute).toThrow(); + }); + + it('should be converted md[value] bind attr to html and set the result to the element content', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' }, + scope: { text: '# abc' } + }); + + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + + fixture.debugElement.componentInstance.text = '## abc'; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + }); + + it('should be converted md[value] bind attr to html and set the result to the element content (whit options)', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' }, + scope: { text: '# abc', options: { noHeaderId: true } } + }); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + + mdDirective.value = '## abc'; + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + + mdDirective.noHeaderId = false; + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + }); + + it('should be converted md[value] bind attr to html and set the result to the element content (trimEachLine directive)', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' }, + scope: { text: ' # a b c ', options: { trimEachLine: 'space' } } + }); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

a b c

'); + + fixture.debugElement.componentInstance.options.trimEachLine = 'tab'; + fixture.debugElement.componentInstance.text = '\t#\ta\tb\tc\t'; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

a b c

'); + + }); + + it('should be converted div[md] bind attr to html and set the result to the element content', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '
' }, + scope: { text: '# abc' } + }); + + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + + fixture.debugElement.componentInstance.text = '## abc'; + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + }); + + it('should be the mdDirective.type to be equal to MdDirective.TYPES.CONTENT', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '# abc' } + }); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + + expect(mdDirective.type).toBe(MdDirective.TYPES[MdDirective.TYPES.CONTENT].toLowerCase()); + expect(mdDirective.status).toBe(MdDirective.STATUSES[MdDirective.STATUSES.READY].toLowerCase()); + }); + + it('should be converted element md content to html and set the result to the element content', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '# abc' } + }); + + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + }); + + it('should be converted element md content to html and set the result to the element content (whit options)', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '# abc' } + }); + + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + }); + + it('should be set get options', () => { + let fixture = $.createFixture(mdDirectiveModuleMetadata, { + metadata: { template: '' }, + scope: { options: { tables: true } } + }); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + + expect(mdDirective.options.noHeaderId).toBeTruthy(); + expect(mdDirective.noHeaderId).toBeTruthy(); + expect(mdDirective.options.trimEachLine).toBe('space'); + expect(mdDirective.trimEachLine).toBe('space'); + expect(mdDirective.options.tables).toBeTruthy(); + expect(mdDirective.tables).toBeTruthy(); + }); + +}); \ No newline at end of file diff --git a/test/md.pipe.spec.ts b/test/md.pipe.spec.ts new file mode 100644 index 0000000..cc97305 --- /dev/null +++ b/test/md.pipe.spec.ts @@ -0,0 +1,58 @@ +import { TestModuleMetadata, ComponentFixtureAutoDetect } from '@angular/core/testing'; +import { ConverterOptions, BaseConverterOptions } from '../src/base-converter-options.provider'; +import { MdPipe } from '../src/md.pipe'; +import $ from './utils'; + +let mdPipeModuleMetadata: TestModuleMetadata = { + declarations: [MdPipe], + providers: [ + {provide: ConverterOptions, useClass: BaseConverterOptions}, + {provide: ComponentFixtureAutoDetect, useValue: true} + ] +}; + +describe('MdPipe', () => { + + it('should transforms "# abc" to "

abc

"', () => { + let pipe = new MdPipe({} as any); + + expect(pipe.transform('# abc')).toBe('

abc

'); + }); + + it('should transforms "# abc" to "

abc

"', () => { + let pipe = new MdPipe({} as any); + + expect(pipe.transform('# abc', {noHeaderId: true})).toBe('

abc

'); + }); + + it('should transforms " # a b c " to "

a b c

"', () => { + let pipe = new MdPipe({} as any); + + expect(pipe.transform(' # a b c ', {trimEachLine: 'space'})).toBe('

a b c

'); + }); + + it('should transforms "\t#\ta\tb\tc\t" to "

a b c

"', () => { + let pipe = new MdPipe({trimEachLine: 'tab'} as any); + + expect(pipe.transform('\t#\ta\tb\tc\t')).toBe('

a b c

'); + }); + + it('should be converted from md to html after it passes through the pipe', () => { + let fixture = $.createFixture(mdPipeModuleMetadata, { + metadata: {template: '{{ text | md }}'}, + scope: {text: '# abc'} + }); + + expect(fixture.debugElement.nativeElement.innerHTML).toBe('<h1 id="abc">abc</h1>'); + }); + + it('should be converted from md to html after it passes through the pipe ', () => { + let fixture = $.createFixture(mdPipeModuleMetadata, { + metadata: {template: '{{ text | md:options}}'}, + scope: {text: '\t# abc\t', options: {trimEachLine: 'tab'}} + }); + + expect(fixture.debugElement.nativeElement.innerHTML).toBe('<h1 id="abc">abc</h1>'); + }); + +}); diff --git a/test/src.directive.spec.ts b/test/src.directive.spec.ts new file mode 100644 index 0000000..2a1bcfc --- /dev/null +++ b/test/src.directive.spec.ts @@ -0,0 +1,112 @@ +import { HttpModule, Http, BaseRequestOptions, Response, ResponseOptions } from '@angular/http'; +import { MockBackend, MockConnection } from '@angular/http/testing'; +import { TestModuleMetadata } from '@angular/core/testing'; +import { MdDirective } from '../src/md.directive'; +import { SrcDirective } from '../src/src.directive'; +import { ConverterOptions, BaseConverterOptions } from '../src/base-converter-options.provider'; +import $ from './utils'; + +let srcDirectiveModuleMetadata: TestModuleMetadata = { + declarations: [MdDirective, SrcDirective], + providers: [ + { provide: ConverterOptions, useClass: BaseConverterOptions }, + BaseRequestOptions, + MockBackend, + { + provide: Http, useFactory: (backend, options) => { + return new Http(backend, options); + }, deps: [MockBackend, BaseRequestOptions] + } + + ], + imports: [HttpModule] +}; + + +describe('SrcDirective', () => { + + it('should be the mdDirective.type to be equal to MdDirective.TYPES.SRC', () => { + let fixture = $.createFixture(srcDirectiveModuleMetadata, { metadata: { template: '' } }); + let mockBackend = fixture.debugElement.injector.get(MockBackend); + let mdDirective: MdDirective = fixture.debugElement.children[0].injector.get(MdDirective); + mockBackend.connections.subscribe(connection => { + connection.mockRespond(new Response(new ResponseOptions({ body: '# abc' }))); + }); + + fixture.detectChanges(); + expect(mdDirective.type).toBe(MdDirective.TYPES[MdDirective.TYPES.SRC].toLowerCase()); + expect(mdDirective.status).toBe(MdDirective.STATUSES[MdDirective.STATUSES.READY].toLowerCase()); + }); + + it('should be request to md[src] url over http and converted the response md data to html and set the result to the element content', () => { + let fixture = $.createFixture(srcDirectiveModuleMetadata, { metadata: { template: '' } }); + let mockBackend = fixture.debugElement.injector.get(MockBackend); + + mockBackend.connections.subscribe(connection => { + connection.mockRespond(new Response(new ResponseOptions({ body: '# abc' }))); + }); + + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + }); + + it('should be request to md[src] url over http and converted the response md data to html and set the result to the element content (whit options)', () => { + let fixture = $.createFixture(srcDirectiveModuleMetadata, { metadata: { template: '' } }); + let mockBackend = fixture.debugElement.injector.get(MockBackend); + + mockBackend.connections.subscribe(connection => { + connection.mockRespond(new Response(new ResponseOptions({ body: '\t # abc\t ' }))); + }); + + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

abc

'); + }); + + it('should be request and converted after binding second time', () => { + let fixture = $.createFixture<{ url: string }>(srcDirectiveModuleMetadata, { metadata: { template: '' }, scope: { url: 'TEST.md' } }); + let mockBackend = fixture.debugElement.injector.get(MockBackend); + let srcDirective: SrcDirective = fixture.debugElement.children[0].injector.get(SrcDirective); + + mockBackend.connections.subscribe((connection: MockConnection) => { + switch (connection.request.url) { + case 'TEST.md': + connection.mockRespond(new Response(new ResponseOptions({ body: '# Test' }))); + break; + case 'TEST2.md': + connection.mockRespond(new Response(new ResponseOptions({ body: '# Test 2' }))); + break; + default: + connection.mockRespond(new Response(new ResponseOptions({ status: 404, body: '404 Page Not Found!' }))); + } + }); + + fixture.detectChanges(); + expect(srcDirective.src).toBe('TEST.md'); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

Test

'); + + fixture.componentInstance.url = 'TEST2.md'; + fixture.detectChanges(); + expect(srcDirective.src).toBe('TEST2.md'); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

Test 2

'); + + }); + + it('should be request same url two time and get different response', () => { + let fixture = $.createFixture<{ url: string }>(srcDirectiveModuleMetadata, { metadata: { template: '' }, scope: { url: 'TEST.md' } }); + let mockBackend = fixture.debugElement.injector.get(MockBackend); + let srcDirective: SrcDirective = fixture.debugElement.children[0].injector.get(SrcDirective); + let count = 1; + mockBackend.connections.subscribe((connection: MockConnection) => { + connection.mockRespond(new Response(new ResponseOptions({ body: `# Test ${count}` }))); + count++; + }); + + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

Test 1

'); + + srcDirective.load(); + expect(fixture.debugElement.nativeElement.children[0].innerHTML).toBe('

Test 2

'); + }); + + +}); \ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..525be2f --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "sourceMap": true + }, + "include": [ + "./*.ts" + ] +} diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 0000000..85cd911 --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,28 @@ +import $ from '../src/utils'; + +describe('Utils', () => { + + describe('trimEachLine', () => { + + it('should be get text and return text whit out trim', () => { + expect($.trimEachLine('\t# abc \n ## def\t')).toBe('\t# abc \n ## def\t'); + }); + + it('should be get void 0 and return void 0', () => { + expect($.trimEachLine(void 0)).toBe(void 0); + }); + + it('should be get text and return trim text', () => { + expect($.trimEachLine('\t# abc \n ## def\t', true)).toBe('# abc\n## def'); + }); + + it('should be get text and return trim space text', () => { + expect($.trimEachLine('\t# abc \n ## def\t', 'space')).toBe('\t# abc\n## def\t'); + }); + + it('should be get text and return trim tab text', () => { + expect($.trimEachLine('\t# abc \n ## def\t', 'tab')).toBe('# abc \n ## def'); + }); + }); + +}); \ No newline at end of file diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..808918f --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,23 @@ +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { TestBed, ComponentFixture, TestModuleMetadata } from '@angular/core/testing'; + +export default class Utils { + + static createFixture(moduleMetadata: TestModuleMetadata, component: any): ComponentFixture { + if (typeof component === 'object') component = this.createComponent(component); + + moduleMetadata = Object.assign({declarations: [], schemas: [NO_ERRORS_SCHEMA]}, moduleMetadata); + if (moduleMetadata.declarations.indexOf(component) === -1) moduleMetadata.declarations.push(component); + + return TestBed.configureTestingModule(moduleMetadata).createComponent(component); + } + + static createComponent({ + metadata = {}, Class = class { + }, scope = {} + }: {Class?: Function, scope?: {[key: string]: any}, metadata?: Component}) { + Object.assign((Class as Function).prototype, scope); + return Component(metadata)(Class); + } + +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..aacc182 --- /dev/null +++ b/tslint.json @@ -0,0 +1,123 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "member-access": false, + "member-ordering": [ + true, + "public-before-private", + "static-before-instance", + "variables-before-functions" + ], + "no-any": false, + "no-inferrable-types": false, + "no-internal-module": true, + "no-var-requires": false, + "typedef": false, + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + }, + { + "call-signature": "space", + "index-signature": "space", + "parameter": "space", + "property-declaration": "space", + "variable-declaration": "space" + } + ], + + "ban": false, + "curly": false, + "forin": true, + "label-position": true, + "no-arg": true, + "no-bitwise": true, + "no-conditional-assignment": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-null-keyword": false, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "radix": true, + "switch-default": true, + "triple-equals": [ + true, + "allow-null-check" + ], + "indent": [ + true, + "spaces" + ], + "no-require-imports": false, + "no-trailing-whitespace": true, + "object-literal-sort-keys": false, + "trailing-comma": [ + true, + { + "multiline": false, + "singleline": "never" + } + ], + + "align": false, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "interface-name": false, + "jsdoc-format": true, + "no-consecutive-blank-lines": false, + "one-line": [ + false, + "check-open-brace", + "check-catch", + "check-else", + "check-finally", + "check-whitespace" + ], + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "semicolon": [true, "always"], + "variable-name": [ + true, + "check-format", + "allow-leading-underscore", + "ban-keywords" + ], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "import-destructuring-spacing": true + } +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..d1edf5a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,66 @@ +let webpack = require('webpack'), + CleanWebpackPlugin = require('clean-webpack-plugin'), + PACKAGE = require('./package.json'), + banner = `/** + * Angular 2 Markdown Module (https://github.com/yisraelx/ng2-md) + * @version ${PACKAGE.version} + * @license MIT (https://github.com/yisraelx/ng2-md/blob/master/LICENSE) + * @copyright Yisrael Eliav (https://github.com/yisraelx) + */`; + +module.exports = { + + entry: { + "ng2-md.umd": "./src/index.ts", + "ng2-md.umd.min": "./src/index.ts" + }, + + output: { + path: __dirname + "/bundles", + filename: "[name].js", + libraryTarget: 'umd', + library: ['ng', 'md'], + umdNamedDefine: false + }, + + resolve: { + extensions: ['', '.js', '.ts'] + }, + + plugins: [ + new CleanWebpackPlugin('./bundles'), + new webpack.optimize.UglifyJsPlugin({ + include: /\.min\.js$/, minimize: true + }), + new webpack.BannerPlugin(banner, {raw: true, entryOnly: true}) + ], + + module: { + loaders: [ + {test: /\.ts$/, loader: "ts-loader"} + ] + }, + + ts: { + compilerOptions: { + declaration: false + } + }, + + externals: { + '@angular/core': { + root: ['ng', 'core'], + amd: '@angular/core', + commonjs: '@angular/core', + commonjs2: '@angular/core' + }, + '@angular/http': { + root: ['ng', 'http'], + amd: '@angular/http', + commonjs: '@angular/http', + commonjs2: '@angular/http' + }, + 'showdown': 'showdown' + } + +}; \ No newline at end of file