├── example.png ├── .gitignore ├── src ├── ramz-logo.png ├── components │ ├── preview.js │ ├── subMenuItem.js │ ├── topBar.js │ ├── menuItems.js │ └── editor.js ├── scss │ ├── _variables.scss │ ├── _base.scss │ └── app.scss ├── icons │ ├── preview.svg │ ├── download.svg │ └── text-direction.svg └── app.js ├── public ├── 29ca6287f19cf37d65cc13b4fa82b7bf.png └── index.html ├── README.md ├── package.json └── webpack.config.js /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedYoussouf/ramz/HEAD/example.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | .sass-cache/ 4 | *.css.map 5 | *.log 6 | -------------------------------------------------------------------------------- /src/ramz-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedYoussouf/ramz/HEAD/src/ramz-logo.png -------------------------------------------------------------------------------- /public/29ca6287f19cf37d65cc13b4fa82b7bf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MohamedYoussouf/ramz/HEAD/public/29ca6287f19cf37d65cc13b4fa82b7bf.png -------------------------------------------------------------------------------- /src/components/preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Preview = (props) => ( 4 |
{props.toShow}
5 | ); 6 | 7 | export default Preview; 8 | -------------------------------------------------------------------------------- /src/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | $white-color: #fff; 3 | $black-color: #000; 4 | $top-menu-height: 50px; 5 | $top-menu-bg: #2F2933; 6 | $top-menu-btn-height: 50px; 7 | 8 | $bg: #fff; 9 | $text-color: #000; 10 | $primary-color: #FF686B; 11 | $primary-font: 'Droid Arabic Naskh', sans-serif, tahoma ; 12 | $secondary-font: 'Noto Kufi Arabic', sans-serif, tahoma ; 13 | -------------------------------------------------------------------------------- /src/components/subMenuItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SubMenuItem = (props) => { 4 | return ( 5 |
  • 6 | 11 |
  • 12 | ) 13 | }; 14 | 15 | export default SubMenuItem; 16 | -------------------------------------------------------------------------------- /src/components/topBar.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | import MenuItem from './menuItems'; 4 | import Logo from '../ramz-logo.png'; 5 | 6 | 7 | const TopBar = (props) => { 8 | return ( 9 | 14 | ); 15 | } 16 | 17 | export default TopBar; 18 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
    21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/menuItems.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SubMenuItem from './subMenuItem'; 3 | 4 | let subItem = null; 5 | let subItemWrapper = null; 6 | const menuItem = (props) => { 7 | if(props.item.submenu != undefined) { 8 | subItemWrapper = 9 | } else { 10 | subItemWrapper = null; 11 | } 12 | 13 | return ( 14 |
  • 15 | 22 | {subItemWrapper} 23 |
  • 24 | ) 25 | }; 26 | 27 | export default menuItem; 28 | -------------------------------------------------------------------------------- /src/components/editor.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | let editorContent; 4 | // Editor compponent 5 | class Editor extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | content: 'أكتب ما في ذهنك!' 10 | }; 11 | 12 | this.onInputChange = this.onInputChange.bind(this); 13 | } 14 | 15 | render() { 16 | return ( 17 |
    22 | أكتب شيئًا ما. 23 |
    24 | ); 25 | }; 26 | 27 | componentDidMount() { 28 | 29 | editorContent = this.refs.editor.innerText; 30 | this.onInputChange(this.state.content); 31 | 32 | } 33 | 34 | 35 | onInputChange(value) { 36 | editorContent = this.refs.editor.innerText; 37 | this.setState({content: editorContent}); 38 | this.props.content(editorContent); 39 | 40 | } 41 | } 42 | 43 | export default Editor; 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ramz 2 | > Markdown editor that understands Arabic 3 | ‏مُحرّر ماركداون يفهم اللغة العربيّة 4 | [https://ramz.netlify.com](https://ramz.netlify.com/) 5 | 6 | ![](example.png) 7 | 8 | ### Features | المُميّزات 9 | - Export files as txt/HTML 10 | - ‏ HTML و txt تصدير الملفّات بصيغتي 11 | - Live preview 12 | - العرض الحيّ للتّعديلات 13 | - Clean design 14 | - تصميم أنيق 15 | - RTL writing direction 16 | - دعم الكتابة من اليمين إلى اليسار 17 | - More is coming! 18 | - والمزيد قادم 19 | 20 | ### Used resources 21 | - [Shawdown](https://github.com/showdownjs/showdown) 22 | - [React](https://github.com/facebook/react) 23 | 24 | ### Development 25 | First make sure that [node.js](https://nodejs.org) and [npm](https://www.npmjs.com/) are installed on your device. 26 | then run: 27 | 28 | npm install 29 | 30 | To install dependencies from `package.json` file. 31 | Run the development server with hot reload at [localhost:8080](localhost:8080): 32 | 33 | npm start 34 | 35 | To build run: 36 | 37 | npm run build 38 | 39 | That is it, happy coding :) 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ramz", 3 | "version": "1.0.0", 4 | "description": "Ramz markdown editor", 5 | "main": "app.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "start": "webpack-dev-server --inline --hot" 9 | }, 10 | "author": "Mohamed Youssouf", 11 | "license": "ISC", 12 | "dependencies": { 13 | "html-to-react": "^1.2.11", 14 | "json-schema-traverse": "^0.3.1", 15 | "normalize-scss": "^7.0.0", 16 | "react": "^15.6.1", 17 | "react-dom": "^15.6.1", 18 | "react-helmet": "^5.2.0", 19 | "react-icons": "^2.2.5", 20 | "showdown": "^1.7.2" 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^6.22.1", 24 | "babel-loader": "^6.2.10", 25 | "babel-preset-es2015": "^6.22.0", 26 | "babel-preset-react": "^6.22.0", 27 | "css-loader": "^0.28.4", 28 | "file-loader": "^1.1.11", 29 | "node-sass": "^4.5.3", 30 | "sass-loader": "^6.0.6", 31 | "style-loader": "^0.18.2", 32 | "svg-inline-loader": "^0.8.0", 33 | "webpack": "^2.7.0", 34 | "webpack-cli": "^2.0.11", 35 | "webpack-dev-server": "^2.11.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: './src/app.js', 6 | output: { 7 | path: path.join(__dirname, 'public'), 8 | // publicPath: "/public", 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /.jsx?$/, 15 | loader: 'babel-loader', 16 | exclude: /node_modules/, 17 | query: { 18 | presets: ['es2015', 'react'] 19 | } 20 | }, 21 | { 22 | test: /\.scss$/, 23 | loader: 'style-loader!css-loader!sass-loader' 24 | }, 25 | { 26 | test: /\.(png|jpg|gif)$/, 27 | use: [ 28 | { 29 | loader: 'file-loader', 30 | } 31 | ] 32 | }, 33 | { 34 | test: /\.svg$/, 35 | loader: 'svg-inline-loader' 36 | } 37 | ] 38 | }, 39 | devServer: { 40 | historyApiFallback: true, 41 | contentBase: 'public', 42 | hot: true 43 | 44 | }, 45 | // watch: true 46 | }; 47 | -------------------------------------------------------------------------------- /src/scss/_base.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | html { 7 | font-family: Arial, Helvetica, sans-serif, tahoma; 8 | line-height: 27px; 9 | } 10 | // body { 11 | // } 12 | 13 | hr { 14 | margin-top: 20px; 15 | margin-bottom: 20px; 16 | border: 0; 17 | border-top: 1px solid #dadada; 18 | } 19 | 20 | a { 21 | color: darken($primary-color, 5); 22 | text-decoration: none; 23 | &:focus, &:hover { 24 | color: darken($primary-color, 10); 25 | } 26 | } 27 | 28 | 29 | // Typography 30 | 31 | h1, h2, h3, h4, h5, h6 { 32 | font-family: $secondary-font; 33 | margin-top: 0; 34 | margin-bottom: 25px; 35 | } 36 | 37 | p { 38 | font-size: 15px; 39 | // line-height: 30px; 40 | 41 | } 42 | 43 | pre { 44 | display: block; 45 | direction: ltr; 46 | text-align: left; 47 | margin: 0px 0 15px; 48 | padding: 16px 15px; 49 | background-color: #eeeeee; 50 | color: #000; 51 | font-size: 15px; 52 | word-break: break-all; 53 | word-wrap: break-word; 54 | border-radius: 5px; 55 | line-height: 1.5em; 56 | 57 | code { 58 | padding: 0; 59 | font-size: inherit; 60 | color: inherit; 61 | white-space: pre-wrap; 62 | } 63 | } 64 | 65 | blockquote { 66 | padding: 10px 20px; 67 | margin: 0 0 20px; 68 | font-size: 17.5px; 69 | border-right: 5px solid #eee; 70 | color: #868686; 71 | 72 | p { 73 | font-size: inherit; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/icons/preview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/scss/app.scss: -------------------------------------------------------------------------------- 1 | // Import fonts 2 | @import url(https://fonts.googleapis.com/earlyaccess/notokufiarabic.css); 3 | @import url(https://fonts.googleapis.com/earlyaccess/droidarabicnaskh.css); 4 | 5 | @import '../../node_modules/normalize-scss/sass/normalize/import-now'; 6 | @import 'variables'; 7 | @import 'base'; 8 | 9 | html, body { 10 | height:100%; 11 | margin:0; 12 | } 13 | 14 | body { 15 | font-family: $primary-font; 16 | background-color: $bg; 17 | color: $text-color; 18 | } 19 | 20 | 21 | 22 | // @mixin heading-type ($heading) { 23 | // #{$heading} { 24 | // position: relative; 25 | // 26 | // &:before { 27 | // content: '#{$heading}'; 28 | // font-size: 25px; 29 | // font-weight: normal; 30 | // font-variant: all-small-caps; 31 | // position: absolute; 32 | // top: 2px; 33 | // right: -50px; 34 | // } 35 | // } 36 | // } 37 | // 38 | // @for $i from 1 through 6 { 39 | // @include heading-type('h#{$i}'); 40 | // } 41 | #app { 42 | display: flex; 43 | flex-direction: column; 44 | height: 100%; 45 | } 46 | 47 | .container { 48 | height: 100%; 49 | .top_menu { 50 | min-height: $top-menu-height; 51 | margin: 0; 52 | // margin-bottom: 10px; 53 | padding: 0; 54 | background-color: $top-menu-bg; 55 | list-style-type: none; 56 | font-family: $secondary-font; 57 | color: #5C5855; 58 | text-align: center; 59 | box-shadow: 0 0 5px 1px rgba($black-color, .08); 60 | > li { 61 | float: right; 62 | position: relative; 63 | > button { 64 | height: $top-menu-btn-height; 65 | line-height: $top-menu-btn-height; 66 | padding: 0 25px; 67 | background: $primary-color; 68 | border: none; 69 | outline: none; 70 | border-radius: 0px; 71 | color: $white-color; 72 | font-family: inherit; 73 | font-size: 13px; 74 | font-weight: bold; 75 | position: relative; 76 | cursor: pointer; 77 | transition: .3s ease; 78 | 79 | span { 80 | vertical-align: middle; 81 | } 82 | 83 | svg { 84 | fill: $white-color; 85 | width: 20px; 86 | vertical-align: middle; 87 | } 88 | 89 | &.btn-preview { 90 | background-color: rgba($white-color, .25); 91 | svg { 92 | fill: $white-color; 93 | } 94 | } 95 | 96 | &:focus { 97 | + .sub_menu { 98 | visibility: visible; 99 | } 100 | } 101 | } 102 | .sub_menu { 103 | visibility: hidden; 104 | margin: 0; 105 | padding: 0; 106 | list-style-type: none; 107 | min-width: 220px; 108 | position: absolute; 109 | top: 50px; 110 | right: 0; 111 | &:before { 112 | content: ''; 113 | border: 7px solid transparent; 114 | border-bottom-color: #000; 115 | position: absolute; 116 | top: -14px; 117 | right: 23px; 118 | } 119 | li { 120 | display: block; 121 | button { 122 | display: block; 123 | margin: 0; 124 | width: 100%; 125 | padding: 18px 25px; 126 | background-color: #000; 127 | color: #fff; 128 | border: none; 129 | font-family: inherit; 130 | font-weight: bold; 131 | font-size: 12px; 132 | text-align: right; 133 | transition: all .3s ease; 134 | &:focus, &:hover { 135 | background-color: $primary-color; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | .doc-title { 142 | height: $top-menu-height; 143 | line-height: $top-menu-height; 144 | padding: 0 25px; 145 | background-color: transparent; 146 | color: #eee; 147 | border: none; 148 | float: right; 149 | width: 60%; 150 | font-family: $primary-font; 151 | } 152 | 153 | #logo { 154 | float: left; 155 | width: 30px; 156 | margin: 8px 0 6px 16px; 157 | } 158 | } 159 | } 160 | 161 | .body-wrapper { 162 | height: calc(100% - 88px); 163 | width: 100%; 164 | display: flex; 165 | flex-direction: row; 166 | align-content: stretch; 167 | 168 | &.toggle-preview { 169 | .editor { 170 | flex: 1; 171 | width: auto; 172 | max-width: none; 173 | } 174 | .preview { 175 | display: block; 176 | } 177 | } 178 | 179 | .editor { 180 | width: 100%; 181 | max-width: 800px; 182 | margin: 0 auto; 183 | padding: 15px; 184 | outline: 1px solid transparent; 185 | overflow: auto; 186 | // transition: all .3s ease-in-out; 187 | } 188 | .preview { 189 | display: none; 190 | flex: 1; 191 | padding: 15px; 192 | border-right: 1px solid #eee; 193 | overflow: auto; 194 | word-break: break-word; 195 | } 196 | } 197 | 198 | .credits { 199 | margin: 0; 200 | padding: 5px 25px; 201 | font-size: 13px; 202 | font-weight: bold; 203 | color: lighten($black-color, 56); 204 | 205 | a { 206 | margin-left: 15px; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/icons/text-direction.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import {Helmet} from "react-helmet"; 4 | 5 | import showdown, {Converter} from 'showdown'; 6 | import HtmlToReactParser from 'html-to-react'; 7 | import previewIcon from './icons/preview.svg'; 8 | 9 | // Import Components 10 | import Editor from './components/editor'; 11 | import Preview from './components/preview'; 12 | import TopBar from './components/topBar'; 13 | // Immport css styles 14 | import './scss/app.scss'; 15 | 16 | let innerHtml, 17 | textDir = 'rtl'; 18 | 19 | class App extends Component { 20 | constructor(props) { 21 | super(props); 22 | this.state = { 23 | inputText: '', 24 | isRTL: true, 25 | title: 'أهلا بالعالم!' 26 | } 27 | this.onLoad = this.onLoad.bind(this) 28 | this.handleChange = this.handleChange.bind(this) 29 | this.contentHandler = this.contentHandler.bind(this) 30 | this.toMarkDown = this.toMarkDown.bind(this) 31 | }; 32 | 33 | render() { 34 | 35 | const htmlToReactParser = new HtmlToReactParser.Parser(); 36 | 37 | innerHtml = htmlToReactParser.parse(this.toMarkDown(this.state.inputText)) 38 | const html = this.toMarkDown(this.state.inputText); 39 | const text = this.state.inputText; 40 | const docTitle = this.state.title; 41 | // Menu Items Object 42 | const menuItems = [ 43 | { 44 | 'id': 1, 45 | 'icon': null, 46 | 'text': 'حفظ', 47 | 'title': 'حفظ المستند', 48 | 'method': null, 49 | 'submenu': [ 50 | { 51 | 'id': 1, 52 | 'title': 'حفظ كمستند نصي', 53 | 'method': () => { 54 | const virtualAnchor = document.createElement('a'); 55 | virtualAnchor.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); 56 | virtualAnchor.setAttribute('download', docTitle); 57 | 58 | virtualAnchor.style.display = 'none'; 59 | document.body.appendChild(virtualAnchor); 60 | 61 | virtualAnchor.click(); 62 | 63 | document.body.removeChild(virtualAnchor); 64 | } 65 | }, 66 | { 67 | // Save as HTML 68 | 'id': 2, 69 | 'title': 'حفظ كصفحة HTML', 70 | 'method': () => { 71 | const virtualAnchor = document.createElement('a'); 72 | const htmlFileTemplate = `${docTitle}
    ${html}
    `; 73 | 74 | virtualAnchor.setAttribute('href', 'data:text/html, ' + htmlFileTemplate); 75 | virtualAnchor.setAttribute('download', docTitle); 76 | 77 | virtualAnchor.style.display = 'none'; 78 | document.body.appendChild(virtualAnchor); 79 | 80 | virtualAnchor.click(); 81 | 82 | document.body.removeChild(virtualAnchor); 83 | 84 | } 85 | }, 86 | ] 87 | }, 88 | { 89 | 'id': 2, 90 | 'icon': previewIcon, 91 | 'title': 'استعراض النتيجة', 92 | 'method' () { 93 | var element = document.getElementById("body-wrapper"); 94 | 95 | if (element.classList) { 96 | element.classList.toggle("toggle-preview"); 97 | } else { 98 | var classes = element.className.split(" "); 99 | var i = classes.indexOf("toggle-preview"); 100 | 101 | if (i >= 0) 102 | classes.splice(i, 1); 103 | else 104 | classes.push("toggle-preview"); 105 | element.className = classes.join(" "); 106 | } 107 | } 108 | }, 109 | // { 110 | // 'id': 4, 111 | // 'icon': textDirectionIcon, 112 | // 'title': 'اتجاه الكتابة', 113 | // 'method' () { 114 | // if (!self.state.isRTL) { 115 | // textDir = 'ltr'; 116 | // } else { 117 | // textDir = 'rtl'; 118 | // } 119 | // self.setState({isRTL: !self.state.isRTL}); 120 | // console.log(self.state.direction); 121 | // } 122 | // } 123 | ] 124 | return ( 125 |
    126 | 127 | رمز - محرر markdown أونلاين 128 | 129 | 130 | 131 |
    132 | 133 | 134 |
    135 |

    تطوير محمد يوسف رمز مشروع مفتوح مصدر ساهم بتطويره

    136 |
    137 | ); 138 | }; 139 | 140 | componentDidMount() { 141 | this.onLoad; 142 | } 143 | 144 | onLoad(content) { 145 | this.contentHandler(content) 146 | } 147 | 148 | handleChange(event) { 149 | this.setState({title: event.target.value}); 150 | console.log(this.state.title) 151 | } 152 | 153 | contentHandler(value) { 154 | this.setState(prevState => ({inputText: value})); 155 | } 156 | 157 | // Function to convert markdown syntax into html 158 | toMarkDown(input) { 159 | const converter = new Converter(); 160 | return converter.makeHtml(input); 161 | } 162 | } 163 | ReactDOM.render(, document.getElementById('app')); 164 | --------------------------------------------------------------------------------