├── 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 = {subItem = props.item.submenu.map(Item => )}
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 | 
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 |
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 |
92 |
--------------------------------------------------------------------------------
/src/icons/text-direction.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
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 |
--------------------------------------------------------------------------------