Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9b7a2671 | ||
|
|
1a3362f1cd | ||
|
|
9009667c5a | ||
|
|
a59023575b | ||
|
|
b2b1ca2934 | ||
|
|
afd1bbb58a | ||
|
|
de655a105a | ||
|
|
2c1c0b2544 | ||
|
|
ccf5318e4c | ||
|
|
7322abfd55 | ||
|
|
09518bbac2 | ||
|
|
b6a831ec91 | ||
|
|
378cd622c4 | ||
|
|
939e3e59cf | ||
|
|
22e82b1c7f | ||
|
|
e495917356 | ||
|
|
cf4b58cab2 | ||
|
|
0a96014484 | ||
|
|
3e8fd9d6e6 | ||
|
|
a06d29863d | ||
|
|
de24e23127 | ||
|
|
9417e1aa49 | ||
|
|
1f8d26a215 | ||
|
|
e4a0b60f1e | ||
|
|
19612e4236 | ||
|
|
af1e9c609b | ||
|
|
5d18c71475 | ||
|
|
20fc9ae343 | ||
|
|
2e8b9145b7 | ||
|
|
859dec8410 | ||
|
|
8f13d2c856 | ||
|
|
d2a226852b | ||
|
|
9c7a05f616 | ||
|
|
a6e59936e2 | ||
|
|
e3d95eafe1 | ||
|
|
e30568ecd5 | ||
|
|
9ac8ea8f93 | ||
|
|
1bac018961 | ||
|
|
1aba91d6f3 | ||
|
|
fd750558ac | ||
|
|
f2cb7d1e8b | ||
|
|
7870fb06a2 | ||
|
|
9adff7572e | ||
|
|
d50da89403 | ||
|
|
8ddfb93f7a | ||
|
|
b718aa9513 | ||
|
|
7782021c12 | ||
|
|
73b2a85eea | ||
|
|
d34f51a9d0 | ||
|
|
0a2abc080f | ||
|
|
9d1786ab67 | ||
|
|
fb6b4899cd | ||
|
|
0c7549819a | ||
|
|
fd8c58e837 | ||
|
|
5c74be339a | ||
|
|
c88c821002 | ||
|
|
f20f83b936 | ||
|
|
88cc3145c4 | ||
|
|
5d1bf5f624 | ||
|
|
9fa998709d | ||
|
|
0f90626a49 | ||
|
|
40108d5201 | ||
|
|
0dde03cf6f | ||
|
|
83fe8ea02b | ||
|
|
17876fc4b0 | ||
|
|
eca16c8474 | ||
|
|
ff0dfed2e8 | ||
|
|
db8e718002 | ||
|
|
f49f0424d5 | ||
|
|
998ddfd54b | ||
|
|
53e67de669 | ||
|
|
897bb0719a | ||
|
|
d051c84737 | ||
|
|
578df160d1 | ||
|
|
ac335a7a5c | ||
|
|
5868915f2c | ||
|
|
1054c81de3 | ||
|
|
79756602a7 | ||
|
|
8d5f446ccd | ||
|
|
b689d08e71 | ||
|
|
58ae90b574 | ||
|
|
e79444e5f7 | ||
|
|
45eb91765b | ||
|
|
933ede1220 | ||
|
|
506bd42201 | ||
|
|
b3ef0a856b | ||
|
|
732c17ad76 | ||
|
|
8ee22694f1 | ||
|
|
e3909c96bd | ||
|
|
ac97f1d61d | ||
|
|
61fbc69356 | ||
|
|
67cd659712 | ||
|
|
895a81799f | ||
|
|
097bf7d193 | ||
|
|
40cccfdfa9 | ||
|
|
71fc855940 | ||
|
|
9fc05d97d9 | ||
|
|
0a3dfa8d9a | ||
|
|
43016e389a | ||
|
|
8c4284a435 | ||
|
|
3f13c56fb9 | ||
|
|
73e2be8ac8 | ||
|
|
86f706dee7 | ||
|
|
1372a68ecf | ||
|
|
dc37ab3d7a | ||
|
|
e9de7d89fd | ||
|
|
2e4b165b42 | ||
|
|
d03b340985 | ||
|
|
90d579bbcb | ||
|
|
8067985edd | ||
|
|
2d5ed1bb83 | ||
|
|
85bcc107d4 | ||
|
|
3d4285d2dc | ||
|
|
c7282f8c04 | ||
|
|
e06612f5b3 | ||
|
|
e28655a64c | ||
|
|
1db21526e1 | ||
|
|
d709739dc0 | ||
|
|
d9a1e2fd0c | ||
|
|
ac86e415e9 | ||
|
|
5b89abb646 | ||
|
|
ed49b8b10f | ||
|
|
58d02cf4bc | ||
|
|
0ed884f5ad | ||
|
|
e278ebda5d | ||
|
|
103184c8da | ||
|
|
3159ddb0a6 | ||
|
|
fc7369f586 | ||
|
|
11c78eb85e | ||
|
|
5449728cc6 | ||
|
|
7d8e58fb4d | ||
|
|
1d46fd0279 | ||
|
|
cc99b267bb | ||
|
|
cf38f55291 | ||
|
|
1483740189 | ||
|
|
f5e91a6366 | ||
|
|
0e539c0a98 | ||
|
|
d1265c21ee | ||
|
|
9a855a7288 | ||
|
|
d331367223 | ||
|
|
156f1f50bc | ||
|
|
3acd66e5b3 | ||
|
|
876a7c1b1f | ||
|
|
444d7c006d | ||
|
|
10353c9a1d | ||
|
|
22e2be2dc8 | ||
|
|
d3e2436527 | ||
|
|
984f255d8a | ||
|
|
3f508da8e6 | ||
|
|
295ffd640b | ||
|
|
b344f9d7e4 | ||
|
|
ca49c83073 | ||
|
|
d28d74a77a | ||
|
|
1b52fde005 | ||
|
|
31d5320922 | ||
|
|
e871dad863 | ||
|
|
eee2db628a | ||
|
|
4c759913c7 | ||
|
|
4442cbae9d | ||
|
|
6d0d75ab1d | ||
|
|
3f2746cdb7 | ||
|
|
d28c696190 | ||
|
|
eba7510be8 | ||
|
|
d1442bfc93 | ||
|
|
743d941204 | ||
|
|
f82bd966e0 | ||
|
|
aedc9854c1 | ||
|
|
87c5b5bc6b | ||
|
|
9f50eff10a | ||
|
|
3b56e7ca2f | ||
|
|
f64c8082ff | ||
|
|
20be763dc1 | ||
|
|
391a956af8 | ||
|
|
2121a6a4f2 | ||
|
|
dc8f82961a | ||
|
|
d9015804c8 | ||
|
|
135bec17a8 | ||
|
|
35624786ab | ||
|
|
0688a360c7 |
@@ -1,138 +0,0 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
@@ -1,2 +1,38 @@
|
||||
# im_webapp
|
||||
# Single-page chat with Tinode
|
||||
|
||||
Example using [Tinode](https://github.com/tinode/chat/) to build a single-page chat application. The app uses
|
||||
[React](https://facebook.github.io/react/), Google's material design [fonts](https://www.google.com/fonts/)
|
||||
and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). The [Tinode javascript library](https://github.com/tinode/tinode-js/) has no external dependencies. Overall it's a lot like open source WhatsApp or Telegram web apps.
|
||||
|
||||
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist. The app was tested in the latest Chrome & Firefox only.
|
||||
|
||||
Try possibly newer or older version live at https://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `17025550001` through `17025550009`. The demo is reset every night at 2:15am Pacific time.
|
||||
|
||||
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Sample avatar photos from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
||||
|
||||
Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
|
||||
|
||||
## Missing functionality
|
||||
|
||||
* Previews not generated for videos, audio, links or docs.
|
||||
* No indication of some chat statuses (muted, blocked, etc.).
|
||||
* Browser push notifications are not implemented.
|
||||
* No end-to-end encryption.
|
||||
* Editing chat permissions is incomplete.
|
||||
* Emoji support is weak.
|
||||
|
||||
## Responsive design
|
||||
|
||||
### Desktop screenshot
|
||||
|
||||
<p align="center">
|
||||
<img src="web-desktop-2.png" alt="Desktop web: full app" width=866 />
|
||||
</p>
|
||||
|
||||
### Mobile screenshots
|
||||
|
||||
<p align="center">
|
||||
<kbd><img src="web-mob-contacts-1.png" alt="Mobile web: contacts" width=323 /></kbd> <kbd><img src="web-mob-chat-1.png" alt="Mobile web: chat" width=323 /></kbd> <kbd><img src="web-mob-info-1.png" alt="Mobile web: topic info" width=323 /></kbd> <kbd><img src="web-mob-new-chat-1.png" alt="Mobile web: start new chat" width=323 /></kbd>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,753 @@
|
||||
// Basic parser and formatter for very simple text markup. Mostly targeted at
|
||||
// mobile use cases similar to Telegram and WhatsApp.
|
||||
//
|
||||
// Supports:
|
||||
// *abc* -> <b>abc</b>
|
||||
// _abc_ -> <i>abc</i>
|
||||
// ~abc~ -> <del>abc</del>
|
||||
// `abc` -> <tt>abc</tt>
|
||||
// Nested frmatting is supported, e.g. *abc _def_* -> <b>abc <i>def</i></b>
|
||||
//
|
||||
// URLs, @mentions, and #hashtags are extracted and converted into links.
|
||||
//
|
||||
// JSON data representation is inspired by Draft.js raw formatting.
|
||||
|
||||
/*
|
||||
Text:
|
||||
this is *bold*, `code` and _italic_, ~strike~
|
||||
combined *bold and _italic_*
|
||||
an url: https://www.example.com/abc#fragment and another _www.tinode.co_
|
||||
this is a @mention and a #hashtag in a string
|
||||
second #hashtag
|
||||
|
||||
Sample JSON representation of the text above:
|
||||
{
|
||||
"txt": "this is bold, code and italic, strike combined bold and italic an url: https://www.example.com/abc#fragment " +
|
||||
"and another www.tinode.co this is a @mention and a #hashtag in a string second #hashtag",
|
||||
"fmt": [
|
||||
{ "at":8, "len":4,"tp":"ST" },{ "at":14, "len":4, "tp":"CO" },{ "at":23, "len":6, "tp":"EM"},
|
||||
{ "at":31, "len":6, "tp":"DL" },{ "tp":"BR", "len":1, "at":37 },{ "at":56, "len":6, "tp":"EM" },
|
||||
{ "at":47, "len":15, "tp":"ST" },{ "tp":"BR", "len":1, "at":62 },{ "at":120, "len":13, "tp":"EM" },
|
||||
{ "at":71, "len":36, "key":0 },{ "at":120, "len":13, "key":1 },{ "tp":"BR", "len":1, "at":133 },
|
||||
{ "at":144, "len":8, "key":2 },{ "at":159, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":179 },
|
||||
{ "at":187, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":195 }
|
||||
],
|
||||
"ent": [
|
||||
{ "tp":"LN", "data":{ "url":"https://www.example.com/abc#fragment" } },
|
||||
{ "tp":"LN", "data":{ "url":"http://www.tinode.co" } },
|
||||
{ "tp":"MN", "data":{ "val":"mention" } },
|
||||
{ "tp":"HT", "data":{ "val":"hashtag" } }
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
(function(environment) { // closure for web browsers
|
||||
'use strict';
|
||||
|
||||
// Regular expressions for parsing inline formats. Javascript does not support lookbehind,
|
||||
// so it's a bit messy.
|
||||
var INLINE_STYLES = [
|
||||
// Strong = bold, *bold text*
|
||||
{name: "ST", start: /(?:^|\W)(\*)[^\s*]/, end: /[^\s*](\*)(?=$|\W)/},
|
||||
// Emphesized = italic, _italic text_
|
||||
{name: "EM", start: /(?:^|[\W_])(_)[^\s_]/, end: /[^\s_](_)(?=$|[\W_])/},
|
||||
// Deleted, ~strike this though~
|
||||
{name: "DL", start: /(?:^|\W)(~)[^\s~]/, end: /[^\s~](~)(?=$|\W)/},
|
||||
// Code block `this is monospace`
|
||||
{name: "CO", start: /(?:^|\W)(`)[^`]/, end: /[^`](`)(?=$|\W)/}
|
||||
];
|
||||
|
||||
// RegExps for entity extraction (RF = reference)
|
||||
var ENTITY_TYPES = [
|
||||
// URLs
|
||||
{name: "LN", dataName: "url",
|
||||
pack: function(val) {
|
||||
// Check if the protocol is specified, if not use http
|
||||
if (!/^[a-z]+:\/\//i.test(val)) {
|
||||
val = 'http://' + val;
|
||||
}
|
||||
return {url: val};
|
||||
},
|
||||
re: /(https?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,4}\b(?:[-a-zA-Z0-9@:%_\+.~#?&//=]*)/g},
|
||||
// Mentions @user (must be 2 or more characters)
|
||||
{name: "MN", dataName: "val",
|
||||
pack: function(val) { return {val: val.slice(1)}; },
|
||||
re: /\B@(\w\w+)/g},
|
||||
// Hashtags #hashtag, like metion 2 or more characters.
|
||||
{name: "HT", dataName: "val",
|
||||
pack: function(val) { return {val: val.slice(1)}; },
|
||||
re: /\B#(\w\w+)/g}
|
||||
];
|
||||
|
||||
// HTML tag name suggestions
|
||||
var HTML_TAGS = {
|
||||
ST: { name: 'b', isVoid: false },
|
||||
EM: { name: 'i', isVoid: false },
|
||||
DL: { name: 'del', isVoid: false },
|
||||
CO: { name: 'tt', isVoid: false },
|
||||
BR: { name: 'br', isVoid: true },
|
||||
LN: { name: 'a', isVoid: false },
|
||||
MN: { name: 'a', isVoid: false },
|
||||
HT: { name: 'a', isVoid: false },
|
||||
IM: { name: 'img', isVoid: true }
|
||||
};
|
||||
|
||||
// Convert base64-encoded string into Blob.
|
||||
function base64toObjectUrl(b64, contentType) {
|
||||
var bin = atob(b64);
|
||||
var length = bin.length;
|
||||
var buf = new ArrayBuffer(length);
|
||||
var arr = new Uint8Array(buf);
|
||||
for (var i = 0; i < length; i++) {
|
||||
arr[i] = bin.charCodeAt(i);
|
||||
}
|
||||
|
||||
return URL.createObjectURL(new Blob([buf], {type: contentType}));
|
||||
}
|
||||
|
||||
// Helpers for converting Drafty to HTML.
|
||||
var DECORATORS = {
|
||||
ST: { open: function() { return '<b>'; }, close: function() { return '</b>'; }},
|
||||
EM: { open: function() { return '<i>'; }, close: function() { return '</i>'}},
|
||||
DL: { open: function() { return '<del>'; }, close: function() { return '</del>'}},
|
||||
CO: { open: function() { return '<tt>'; }, close: function() { return '</tt>'}},
|
||||
BR: { open: function() { return ''; }, close: function() { return '<br/>'}},
|
||||
LN: {
|
||||
open: function(data) { return '<a href="' + data.url + '">'; },
|
||||
close: function(data) { return '</a>'; },
|
||||
props: function(data) { return { href: data.url, target: "_blank" }; },
|
||||
},
|
||||
MN: {
|
||||
open: function(data) { return '<a href="#' + data.val + '">'; },
|
||||
close: function(data) { return '</a>'; },
|
||||
props: function(data) { return { name: data.val }; },
|
||||
},
|
||||
HT: {
|
||||
open: function(data) { return '<a href="#' + data.val + '">'; },
|
||||
close: function(data) { return '</a>'; },
|
||||
props: function(data) { return { name: data.val }; },
|
||||
},
|
||||
IM: {
|
||||
open: function(data) {
|
||||
var blobUrl = base64toObjectUrl(data.val, data.mime);
|
||||
var res = (data.name ? '<a href="' + blobUrl + '" download="' + data.name + '">' : '') +
|
||||
'<img src="' + blobUrl + '"' +
|
||||
(data.width ? ' width="' + data.width + '"' : '') +
|
||||
(data.height ? ' height="' + data.height + '"' : '') + ' border="0" />';
|
||||
console.log("open: " + res);
|
||||
return res;
|
||||
},
|
||||
close: function(data) {
|
||||
return (data.name ? '</a>' : '');
|
||||
},
|
||||
props: function(data) {
|
||||
var url = base64toObjectUrl(data.val, data.mime);
|
||||
return {
|
||||
src: url,
|
||||
title: data.name,
|
||||
'data-width': data.width,
|
||||
'data-height': data.height,
|
||||
'data-name': data.name,
|
||||
'data-size': (data.val.length * 0.75) | 0,
|
||||
'data-mime': data.mime
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
var Drafty = (function() {
|
||||
|
||||
// Take a string and defined earlier style spans, re-compose them into a tree where each leaf is
|
||||
// a same-style (including unstyled) string. I.e. 'hello *bold _italic_* and ~more~ world' ->
|
||||
// ('hello ', (b: 'bold ', (i: 'italic')), ' and ', (s: 'more'), ' world');
|
||||
//
|
||||
// This is needed in order to clear markup, i.e. 'hello *world*' -> 'hello world' and convert
|
||||
// ranges from markup-ed offsets to plain text offsets.
|
||||
function chunkify(line, start, end, spans) {
|
||||
var chunks = [];
|
||||
|
||||
if (spans.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (var i in spans) {
|
||||
// Get the next chunk from the queue
|
||||
var span = spans[i];
|
||||
|
||||
// Grab the initial unstyled chunk
|
||||
if (span.start > start) {
|
||||
chunks.push({text: line.slice(start, span.start)});
|
||||
}
|
||||
|
||||
// Grab the styled chunk. It may include subchunks.
|
||||
var chunk = {type: span.type};
|
||||
var chld = chunkify(line, span.start + 1, span.end - 1, span.children);
|
||||
if (chld.length > 0) {
|
||||
chunk.children = chld;
|
||||
} else {
|
||||
chunk.text = span.text;
|
||||
}
|
||||
chunks.push(chunk);
|
||||
start = span.end + 1; // '+1' is to skip the formatting character
|
||||
}
|
||||
|
||||
// Grab the remaining unstyled chunk, after the last span
|
||||
if (start < end) {
|
||||
chunks.push({text: line.slice(start, end)});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Same as chunkify but used for formatting.
|
||||
function forEach(line, start, end, spans, formatter, context) {
|
||||
// Add un-styled range before the styled span starts.
|
||||
// Process ranges calling formatter for each range.
|
||||
var result = [];
|
||||
for (var i = 0; i < spans.length; i++) {
|
||||
var span = spans[i];
|
||||
|
||||
// Add un-styled range before the styled span starts.
|
||||
if (start < span.at) {
|
||||
result.push(formatter.call(context, null, undefined, line.slice(start, span.at)));
|
||||
start = span.at;
|
||||
}
|
||||
// Get all spans which are within current span.
|
||||
var subspans = [];
|
||||
for (var si = i + 1; si < spans.length && spans[si].at < span.at + span.len; si++) {
|
||||
subspans.push(spans[si]);
|
||||
i = si;
|
||||
}
|
||||
|
||||
var tag = HTML_TAGS[span.tp] || {};
|
||||
result.push(formatter.call(context, span.tp, span.data,
|
||||
tag.isVoid ? null : forEach(line, start, span.at + span.len, subspans, formatter, context)));
|
||||
|
||||
start = span.at + span.len;
|
||||
}
|
||||
|
||||
// Add the last unformatted range.
|
||||
if (start < end) {
|
||||
result.push(formatter.call(context, null, undefined, line.slice(start, end)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Detect starts and ends of formatting spans. Unformatted spans are
|
||||
// ignored at this stage.
|
||||
function spannify(original, re_start, re_end, type) {
|
||||
var result = [];
|
||||
var index = 0;
|
||||
var line = original.slice(0); // make a copy;
|
||||
|
||||
while (line.length > 0) {
|
||||
// match[0]; // match, like '*abc*'
|
||||
// match[1]; // match captured in parenthesis, like 'abc'
|
||||
// match['index']; // offset where the match started.
|
||||
|
||||
// Find the opening token.
|
||||
var start = re_start.exec(line);
|
||||
if (start == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Because javascript RegExp does not support lookbehind, the actual offset may not point
|
||||
// at the markup character. Find it in the matched string.
|
||||
var start_offset = start['index'] + start[0].lastIndexOf(start[1]);
|
||||
// Clip the processed part of the string.
|
||||
line = line.slice(start_offset + 1);
|
||||
// start_offset is an offset within the clipped string. Convert to original index.
|
||||
start_offset += index;
|
||||
// Index now point to the beginning of 'line' within the 'original' string.
|
||||
index = start_offset + 1;
|
||||
|
||||
// Find the matching closing token.
|
||||
var end = re_end ? re_end.exec(line) : null;
|
||||
if (end == null) {
|
||||
break;
|
||||
}
|
||||
var end_offset = end['index'] + end[0].indexOf(end[1]);
|
||||
// Clip the processed part of the string.
|
||||
line = line.slice(end_offset + 1);
|
||||
// Update offsets
|
||||
end_offset += index;
|
||||
// Index now point to the beginning of 'line' within the 'original' string.
|
||||
index = end_offset + 1;
|
||||
|
||||
result.push({
|
||||
text: original.slice(start_offset+1, end_offset),
|
||||
children: [],
|
||||
start: start_offset,
|
||||
end: end_offset,
|
||||
type: type
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Convert linear array or spans into a tree representation.
|
||||
// Keep standalone and nested spans, throw away partially overlapping spans.
|
||||
function toTree(spans) {
|
||||
if (spans.length == 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var tree = [spans[0]];
|
||||
var last = spans[0];
|
||||
for (var i = 1; i < spans.length; i++) {
|
||||
// Keep spans which start after the end of the previous span or those which
|
||||
// are complete within the previous span.
|
||||
|
||||
if (spans[i].start > last.end) {
|
||||
// Span is completely outside of the previous span.
|
||||
tree.push(spans[i]);
|
||||
last = spans[i];
|
||||
} else if (spans[i].end < last.end) {
|
||||
// Span is fully inside of the previous span. Push to subnode.
|
||||
last.children.push(spans[i]);
|
||||
}
|
||||
// Span could partially overlap, ignoring it as invalid.
|
||||
}
|
||||
|
||||
// Recursively rearrange the subnodes.
|
||||
for (var i in tree) {
|
||||
tree[i].children = toTree(tree[i].children);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
// Get a list of entities from a text.
|
||||
function extractEntities(line) {
|
||||
var match;
|
||||
var extracted = [];
|
||||
ENTITY_TYPES.map(function(entity) {
|
||||
while ((match = entity.re.exec(line)) !== null) {
|
||||
extracted.push({
|
||||
offset: match['index'],
|
||||
len: match[0].length,
|
||||
unique: match[0],
|
||||
data: entity.pack(match[0]),
|
||||
type: entity.name});
|
||||
}
|
||||
});
|
||||
|
||||
if (extracted.length == 0) {
|
||||
return extracted;
|
||||
}
|
||||
|
||||
// Remove entities detected inside other entities, like #hashtag in a URL.
|
||||
extracted.sort(function(a,b) {
|
||||
return a.offset - b.offset;
|
||||
});
|
||||
|
||||
var idx = -1;
|
||||
extracted = extracted.filter(function(el) {
|
||||
var result = (el.offset > idx);
|
||||
idx = el.offset + el.len;
|
||||
return result;
|
||||
});
|
||||
|
||||
return extracted;
|
||||
}
|
||||
|
||||
// Convert the chunks into format suitable for serialization.
|
||||
function draftify(chunks, startAt) {
|
||||
var plain = "";
|
||||
var ranges = [];
|
||||
for (var i in chunks) {
|
||||
var chunk = chunks[i];
|
||||
if (!chunk.text) {
|
||||
var drafty = draftify(chunk.children, plain.length + startAt);
|
||||
chunk.text = drafty.txt;
|
||||
ranges = ranges.concat(drafty.fmt);
|
||||
}
|
||||
|
||||
if (chunk.type) {
|
||||
ranges.push({at: plain.length + startAt, len: chunk.text.length, tp: chunk.type});
|
||||
}
|
||||
|
||||
plain += chunk.text;
|
||||
}
|
||||
return {txt: plain, fmt: ranges};
|
||||
}
|
||||
|
||||
// Splice two strings: insert second string into the first one at the given index
|
||||
function splice(src, at, insert) {
|
||||
return src.slice(0, at) + insert + src.slice(at);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Parse plain text into structured representation.
|
||||
*/
|
||||
parse: function(content) {
|
||||
// Make sure we are parsing strings only.
|
||||
if (typeof content != "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Split text into lines. It makes further processing easier.
|
||||
var lines = content.split(/\r?\n/);
|
||||
|
||||
// Holds entities referenced from text
|
||||
var entityMap = [];
|
||||
var entityIndex = {};
|
||||
|
||||
// Processing lines one by one, hold intermediate result in blx.
|
||||
var blx = [];
|
||||
lines.map(function(line) {
|
||||
var spans = [];
|
||||
var entities = [];
|
||||
|
||||
// Find formatted spans in the string.
|
||||
// Try to match each style.
|
||||
INLINE_STYLES.map(function(style) {
|
||||
// Each style could be matched multiple times.
|
||||
spans = spans.concat(spannify(line, style.start, style.end, style.name));
|
||||
});
|
||||
|
||||
var block;
|
||||
if (spans.length == 0) {
|
||||
block = {txt: line};
|
||||
} else {
|
||||
// Sort spans by style occurence early -> late
|
||||
spans.sort(function(a,b) {
|
||||
return a.start - b.start;
|
||||
});
|
||||
|
||||
// Convert an array of possibly overlapping spans into a tree
|
||||
spans = toTree(spans);
|
||||
|
||||
// Build a tree representation of the entire string, not
|
||||
// just the formatted parts.
|
||||
var chunks = chunkify(line, 0, line.length, spans);
|
||||
|
||||
var drafty = draftify(chunks, 0);
|
||||
|
||||
block = {txt: drafty.txt, fmt: drafty.fmt};
|
||||
}
|
||||
|
||||
// Extract entities from the cleaned up string.
|
||||
entities = extractEntities(block.txt);
|
||||
if (entities.length > 0) {
|
||||
var ranges = [];
|
||||
for (var i in entities) {
|
||||
// {offset: match['index'], unique: match[0], len: match[0].length, data: ent.packer(), type: ent.name}
|
||||
var entity = entities[i];
|
||||
var index = entityIndex[entity.unique];
|
||||
if (!index) {
|
||||
index = entityMap.length;
|
||||
entityIndex[entity.unique] = index;
|
||||
entityMap.push({tp: entity.type, data: entity.data});
|
||||
}
|
||||
ranges.push({at: entity.offset, len: entity.len, key: index});
|
||||
}
|
||||
block.ent = ranges;
|
||||
}
|
||||
|
||||
blx.push(block);
|
||||
});
|
||||
|
||||
var result = {txt: ""};
|
||||
|
||||
// Merge lines and save line breaks as BR inline formatting.
|
||||
if (blx.length > 0) {
|
||||
result.txt = blx[0].txt;
|
||||
result.fmt = (blx[0].fmt || []).concat(blx[0].ent || []);
|
||||
|
||||
for (var i = 1; i<blx.length; i++) {
|
||||
var block = blx[i];
|
||||
var offset = result.txt.length + 1;
|
||||
|
||||
result.fmt.push({tp: "BR", len: 1, at: offset - 1});
|
||||
|
||||
result.txt += " " + block.txt;
|
||||
if (block.fmt) {
|
||||
result.fmt = result.fmt.concat(block.fmt.map(function(s) {
|
||||
s.at += offset; return s;
|
||||
}));
|
||||
}
|
||||
if (block.ent) {
|
||||
result.fmt = result.fmt.concat(block.ent.map(function(s) {
|
||||
s.at += offset; return s;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (result.fmt.length == 0) {
|
||||
delete result.fmt;
|
||||
}
|
||||
|
||||
if (entityMap.length > 0) {
|
||||
result.ent = entityMap;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add inline image to drafty content
|
||||
*
|
||||
* @param {Drafty} content object to add image to
|
||||
* @param {integer} at index where the object is inserted
|
||||
* @param {string} mime mime-type of the image, e.g. "image/png"
|
||||
* @param {string} bits base64-encoded image content
|
||||
* @param {integer} width width of the image
|
||||
* @param {integer} height height of the image
|
||||
* @param {string} fname file name suggestion for downloading the image.
|
||||
*/
|
||||
insertImage: function(content, at, mime, base64bits, width, height, fname) {
|
||||
content = content || {txt: " "};
|
||||
content.ent = content.ent || [];
|
||||
content.fmt = content.fmt || [];
|
||||
|
||||
content.fmt.push({
|
||||
at: at,
|
||||
len: 1,
|
||||
key: content.ent.length
|
||||
});
|
||||
content.ent.push({
|
||||
tp: "IM",
|
||||
data: {
|
||||
mime: mime,
|
||||
val: base64bits,
|
||||
width: width,
|
||||
height: height,
|
||||
name: fname
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
attachFile: function(content, mime, base64bits, fname) {
|
||||
content = content || {txt: ""};
|
||||
content.ent = content.ent || [];
|
||||
content.fmt = content.fmt || [];
|
||||
|
||||
content.fmt.push({
|
||||
at: -1,
|
||||
len: 0,
|
||||
key: content.ent.length
|
||||
});
|
||||
|
||||
content.ent.push({
|
||||
tp: "EX",
|
||||
data: {
|
||||
mime: mime,
|
||||
val: base64bits,
|
||||
name: fname
|
||||
}
|
||||
});
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given the structured representation of rich text, convert it to HTML.
|
||||
* No attempt is made to strip pre-existing html markup.
|
||||
* This is potentially unsafe because block.txt may contain malicious
|
||||
* markup.
|
||||
*
|
||||
* @param {drafy} content - structured representation of rich text.
|
||||
*
|
||||
* @return HTML-representation of content
|
||||
*/
|
||||
unsafeToHTML: function(content) {
|
||||
var {txt, fmt, ent} = content;
|
||||
|
||||
var markup = [];
|
||||
if (fmt) {
|
||||
for (var i in fmt) {
|
||||
var range = fmt[i];
|
||||
var tp = range.tp, data;
|
||||
if (!tp) {
|
||||
var entity = ent[range.key];
|
||||
if (entity) {
|
||||
tp = entity.tp;
|
||||
data = entity.data;
|
||||
}
|
||||
}
|
||||
|
||||
if (DECORATORS[tp]) {
|
||||
// Because we later sort in descending order, closing markup must come first.
|
||||
// Otherwise zero-length objects will not be represented correctly.
|
||||
markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)});
|
||||
markup.push({idx: range.at, what: DECORATORS[tp].open(data)});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markup.sort(function(a, b) {
|
||||
return b.idx - a.idx; // in descending order
|
||||
});
|
||||
|
||||
for (var i in markup) {
|
||||
if (markup[i].what) {
|
||||
txt = splice(txt, markup[i].idx, markup[i].what);
|
||||
}
|
||||
}
|
||||
|
||||
return txt;
|
||||
},
|
||||
|
||||
/**
|
||||
* Transform using custom formatting.
|
||||
*
|
||||
* @param {Drafty} content - content to transform.
|
||||
* @param {function} formatter - callback which transforms individual elements
|
||||
* @param {Object} context - context provided to formatter as 'this'.
|
||||
*
|
||||
* @return {Object} context
|
||||
*/
|
||||
format: function(content, formatter, context) {
|
||||
var {txt, fmt, ent} = content;
|
||||
|
||||
txt = txt || "";
|
||||
|
||||
if (!fmt) {
|
||||
return [txt];
|
||||
}
|
||||
|
||||
var spans = [].concat(fmt);
|
||||
|
||||
// Zero values may have been stripped. Restore them.
|
||||
spans.map(function(s) {
|
||||
s.at = s.at || 0;
|
||||
s.len = s.len || 0;
|
||||
});
|
||||
|
||||
// Soft spans first by start index (asc) then by length (desc).
|
||||
spans.sort(function(a, b) {
|
||||
if (a.at - b.at == 0) {
|
||||
return b.len - a.len; // longer one comes first (<0)
|
||||
}
|
||||
return a.at - b.at;
|
||||
});
|
||||
|
||||
// Denormalize entities into spans. Create a copy of the objects to leave
|
||||
// original Drafty object unchanged.
|
||||
spans = spans.map(function(s) {
|
||||
var data;
|
||||
var tp = s.tp;
|
||||
if (!tp) {
|
||||
s.key = s.key || 0;
|
||||
data = ent[s.key].data;
|
||||
tp = ent[s.key].tp;
|
||||
}
|
||||
return {tp: tp, data: data, at: s.at, len: s.len};
|
||||
});
|
||||
|
||||
return forEach(txt, 0, txt.length, spans, formatter, context);
|
||||
},
|
||||
|
||||
/**
|
||||
* Given structured representation of rich text, convert it to plain text.
|
||||
*
|
||||
* @param {drafty} content - content to convert to plain text.
|
||||
*/
|
||||
toPlainText: function(content) {
|
||||
return content.text;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if content has no markup and no entities.
|
||||
*
|
||||
* @param {Drafty} content - content to check for presence of markup.
|
||||
* @returns true is content is plain text, false otherwise.
|
||||
*/
|
||||
isPlainText: function(content, noLineBreaks) {
|
||||
return !(content.fmt || content.ent);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the drafty content has attachments.
|
||||
*
|
||||
* @param {Drafty} content - content to check for attachments.
|
||||
* @returns true if there are attachments.
|
||||
*/
|
||||
hasAttachment: function(content) {
|
||||
if (content.ent && content.ent.length > 0) {
|
||||
for (var i in content.ent) {
|
||||
if (content.ent[i].tp == "EX") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enumerate attachments
|
||||
*
|
||||
* @param {Drafty} content - drafty object to process for attachments.
|
||||
* @param {function} callback - callback to call for each attachment.
|
||||
* @param {Object} content - value of "this" for callback.
|
||||
*/
|
||||
attachments: function(content, callback, context) {
|
||||
if (content.ent && content.ent.length > 0) {
|
||||
for (var i in content.ent) {
|
||||
if (content.ent[i].tp == "EX") {
|
||||
callback.call(context, content.ent[i].data, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Get blob URL for a given entity
|
||||
getBlobUrl: function(entity) {
|
||||
return base64toObjectUrl(entity.val, entity.mime);
|
||||
},
|
||||
|
||||
getAttachmentSize: function(entity) {
|
||||
// Value is base64 encoded. The actual object size is smaller than the encoded length.
|
||||
return entity.val ? (entity.val.length * 0.75) | 0 : 0;
|
||||
},
|
||||
|
||||
getAttachmentType: function(entity) {
|
||||
return entity.mime || "text/plain";
|
||||
},
|
||||
/**
|
||||
* Get HTML tag for a given two-letter style name
|
||||
* @param {string} style - two-letter style, like ST or LN
|
||||
* @returns
|
||||
*/
|
||||
tagName: function(style) {
|
||||
return HTML_TAGS[style] ? HTML_TAGS[style].name : undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* For a given data bundle generate an object with HTML attributes,
|
||||
* for instance, given {url: "http://www.example.com/"} return
|
||||
* {href: "http://www.example.com/"}
|
||||
* @param {string} style - tw-letter style to generate attributes for.
|
||||
* @param {Object} data - data bundle to convert to attributes
|
||||
* @returns object with HTML attributes.
|
||||
*/
|
||||
attrValue: function(style, data) {
|
||||
if (data && DECORATORS[style]) {
|
||||
return DECORATORS[style].props(data);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Content MIME type.
|
||||
* @returns string suitabe for HTTP Content-Type field.
|
||||
*/
|
||||
getContentType: function() {
|
||||
return "text/x-drafty";
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Export for the window object or node; Check that is not already defined.
|
||||
if (typeof(environment.Drafty) === 'undefined') {
|
||||
environment.Drafty = Drafty();
|
||||
}
|
||||
})(this); // 'this' will be a window object for the browsers.
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 164 KiB |