├── tsconfig.json ├── README.md ├── package.json ├── src ├── jsx.d.ts ├── index.tsx └── template.ts ├── .gitignore └── yarn.lock /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsxFactory": "Template.create", 4 | "jsx": "react", 5 | 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "outDir": "dest", 9 | "baseUrl": "src", 10 | "allowJs": false, 11 | "target": "es2017", 12 | "strictNullChecks": true, 13 | "lib": [ "es2017" ] 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | "dest" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ### TypeScript JSX syntax as typed DSL 4 | 5 | This is an example repository for an article. 6 | 7 | #### Run 8 | To run the example: 9 | - Clone this repository; 10 | - Run `yarn` to install dependencies; 11 | - Run `yarn start` to run the example. 12 | 13 |
14 | made with ❤️ by @dempfi 15 |
16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-typescript-dsl", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Ike Ku ", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "tsc; node dest/index.js" 10 | }, 11 | "devDependencies": { 12 | "@types/lodash": "^4.14.88", 13 | "@types/node": "^8.0.57", 14 | "typescript": "^2.6.2" 15 | }, 16 | "dependencies": { 17 | "lodash": "^4.17.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/jsx.d.ts: -------------------------------------------------------------------------------- 1 | /* Picked up by compiler automatically */ 2 | declare namespace JSX { 3 | interface Element { 4 | toMessage(): { 5 | text?: string 6 | attachments?: { 7 | text?: string 8 | title?: string 9 | title_link?: string 10 | author_name?: string 11 | author_icon?: string 12 | color?: string 13 | }[] 14 | } 15 | } 16 | interface IntrinsicElements { 17 | i: {} 18 | message: {} 19 | author: { icon: string } 20 | title: { link?: string } 21 | attachment: { 22 | color?: string 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Framework folders 2 | 3 | /log 4 | /tmp 5 | 6 | # Local configuration 7 | 8 | /.env 9 | /.envrc 10 | /.rbenv-vars 11 | /.rbenv-version 12 | /.ruby-gemset 13 | /.ruby-version 14 | /.rvmrc 15 | 16 | # Build and tool artifacts 17 | 18 | *.DS_Store 19 | *.iml 20 | /.bundle 21 | /bundle 22 | .idea 23 | .vscode 24 | .byebug_history 25 | .ipynb_checkpoints 26 | 27 | # Logs 28 | logs 29 | *.log 30 | npm-debug.log* 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | 37 | # Dependency directories 38 | node_modules 39 | coverage 40 | .nyc_output 41 | jspm_packages 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | .DS_Store 47 | 48 | # Compiled 49 | dest 50 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/lodash@^4.14.88": 6 | version "4.14.88" 7 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.88.tgz#97eaf2dc668f33ed8e511a5b627035f4ea20b0f5" 8 | 9 | "@types/node@^8.0.57": 10 | version "8.0.57" 11 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.57.tgz#e5d8b4dc112763e35cfc51988f4f38da3c486d99" 12 | 13 | lodash@^4.17.4: 14 | version "4.17.4" 15 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 16 | 17 | typescript@^2.6.2: 18 | version "2.6.2" 19 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" 20 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Template from './template' 2 | 3 | interface Story { 4 | title: string 5 | link: string 6 | publishedAt: Date 7 | author: { name: string, avatarURL: string } 8 | } 9 | 10 | const template = (username: string, stories: Story[]) => 11 | 12 | :wave: Hi {username}, check out our recent stories. 13 | 14 | {stories.map(s => 15 | 16 | {s.author.name} 17 | {s.title} 18 | 19 | Published at {s.publishedAt}. 20 | 21 | )} 22 | 23 | 24 | const stories: Story[] = [ 25 | { 26 | title: 'Story title', 27 | link: 'https://google.com', 28 | publishedAt: new Date(), 29 | author: { 30 | name: 'Ike Ku', 31 | avatarURL: 'https://avatars2.githubusercontent.com/u/4568573' 32 | } 33 | } 34 | ] 35 | 36 | console.log(template('User', stories).toMessage()) 37 | -------------------------------------------------------------------------------- /src/template.ts: -------------------------------------------------------------------------------- 1 | import { flatten } from 'lodash' 2 | 3 | type Kinds = keyof JSX.IntrinsicElements // Tag names 4 | type Attrubute = JSX.IntrinsicElements[K] // Tag attributes 5 | 6 | const isElement = (e: any): e is Element => 7 | e && e.kind 8 | 9 | const is = (k: K, e: string | Element): e is Element => 10 | isElement(e) && e.kind === k 11 | 12 | /* Concat all direct child nodes that aren't Elements (strings) */ 13 | const buildText = (e: Element) => 14 | e.children.filter(i => !isElement(i)).join('') 15 | 16 | const buildTitle = (e: Element<'title'>) => ({ 17 | title: buildText(e), 18 | title_link: e.attributes.link 19 | }) 20 | 21 | const buildAuthor = (e: Element<'author'>) => ({ 22 | author_name: buildText(e), 23 | author_icon: e.attributes.icon 24 | }) 25 | 26 | const buildAttachment = (e: Element<'attachment'>) => { 27 | const authorNode = e.children.find(i => is('author', i)) 28 | const author = authorNode ? buildAuthor(>authorNode) : {} 29 | 30 | const titleNode = e.children.find(i => is('title', i)) 31 | const title = titleNode ? buildTitle(>titleNode) : {} 32 | 33 | return { text: buildText(e), ...title, ...author, ...e.attributes } 34 | } 35 | 36 | class Element { 37 | children: Array> 38 | 39 | constructor( 40 | public kind: K, 41 | public attributes: Attrubute, 42 | children: Array> 43 | ) { 44 | this.children = flatten(children) 45 | } 46 | 47 | /* 48 | * Convert this Element to actual Slack message 49 | * only if it is a higher level Element — . 50 | */ 51 | toMessage() { 52 | if (!is('message', this)) return {} 53 | const attachments = this.children.filter(i => is('attachment', i)).map(buildAttachment) 54 | return { attachments, text: buildText(this) } 55 | } 56 | } 57 | 58 | export const create = (kind: K, attributes: Attrubute, ...children) => { 59 | switch (kind) { 60 | case 'i': return `_${children.join('')}_` 61 | default: return new Element(kind, attributes, children) 62 | } 63 | } 64 | --------------------------------------------------------------------------------