Compare commits

..
18 changed files with 1486 additions and 371 deletions

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
path: dist
retention-days: 1
- name: Get PR info
uses: actions/github-script@v6.1.1
uses: actions/github-script@v6.1.0
with:
script: |
var fs = require('fs');
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
# workflow_run action (https://github.com/actions/download-artifact/issues/60)
# so instead we get this mess:
- name: Download artifact
uses: actions/github-script@v6.1.1
uses: actions/github-script@v6.1.0
with:
script: |
var artifacts = await github.rest.actions.listWorkflowRunArtifacts({
@@ -51,7 +51,7 @@ jobs:
run: unzip -d dist previewbuild.zip && rm previewbuild.zip && unzip pr.json.zip && rm pr.json.zip
- name: Read PR Info
id: readctx
uses: actions/github-script@v6.1.1
uses: actions/github-script@v6.1.0
with:
script: |
var fs = require('fs');
+2 -11
View File
@@ -64,11 +64,10 @@ jobs:
NETLIFY_DEPLOY_TO_PROD: true
push-to-dockerhub:
name: Push Docker image to Docker Hub, ghcr
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3.0.2
@@ -81,19 +80,11 @@ jobs:
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to the Container registry
uses: docker/login-action@v2.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4.0.1
with:
images: |
${{ secrets.DOCKER_USERNAME }}/cinny
ghcr.io/${{ github.repository }}
images: ajbura/cinny
- name: Build and push Docker image
uses: docker/build-push-action@v3.1.1
with:
+1 -6
View File
@@ -36,9 +36,4 @@ It is not always possible to phrase every change in such a manner, but it is des
Also, we use [ESLint](https://eslint.org/) for clean and stylistically consistent code syntax, so make sure your pull request follow it.
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
## Helpful links
- [BEM methodology](http://getbem.com/introduction/)
- [Atomic design](https://bradfrost.com/blog/post/atomic-web-design/)
- [Matrix JavaScript SDK documentation](https://matrix-org.github.io/matrix-js-sdk/19.2.0/index.html)
**For any query or design discussion, join our [Matrix room](https://matrix.to/#/#cinny:matrix.org).**
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021-present Ajay Bura (ajbura)
Copyright (c) 2021 Ajay Bura (ajbura)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+33 -29
View File
@@ -1,5 +1,9 @@
# Cinny
<p>
<p align="center">
<img src="https://raw.githubusercontent.com/ajbura/cinny/dev/public/res/svg/cinny.svg?sanitize=true"
height="16">
<span><b>Cinny</b></span>
</p>
<p align="center">
<a href="https://github.com/ajbura/cinny/releases">
<img alt="GitHub release downloads" src="https://img.shields.io/github/downloads/ajbura/cinny/total?logo=github&style=social"></a>
<a href="https://hub.docker.com/r/ajbura/cinny">
@@ -12,30 +16,21 @@
<img alt="Sponsor Cinny" src="https://img.shields.io/opencollective/all/cinny?logo=opencollective&style=social"></a>
</p>
A Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have an instant messaging application that is easy on people and has a modern touch.
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
**Cinny** is a Matrix client focusing primarily on simple, elegant and secure interface. The main goal is to have a client that is easy on end user
and feels a modern chat application.
- [Contributing](./CONTRIBUTING.md)
- [Roadmap](https://github.com/ajbura/cinny/projects/11)
## Getting started
Web app is available at https://app.cinny.in and gets updated on each new release. The `dev` branch is continuously deployed at https://dev.cinny.in but keep in mind that it could have things broken.
## Building and Running
You can also download our desktop app from [cinny-desktop repository](https://github.com/cinnyapp/cinny-desktop).
### Running pre-compiled
To host Cinny on your own, download tarball of the app from [GitHub release](https://github.com/cinnyapp/cinny/releases/latest).
You can serve the application with a webserver of your choice by simply copying `dist/` directory to the webroot.
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by:
```
docker pull ajbura/cinny
```
or [ghcr image](https://github.com/cinnyapp/cinny/pkgs/container/cinny) by:
```
docker pull ghcr.io/cinnyapp/cinny:latest
```
A tarball of pre-compiled version of the app is provided with each [release](https://github.com/ajbura/cinny/releases).
You can serve the application with a webserver of your choosing by simply copying `dist/` directory to the webroot.
<details>
<summary>PGP Public Key to verify tarball</summary>
<summary>PGP Public Key to verify pre-compiled tarball</summary>
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -82,39 +77,48 @@ UeGsouhyuITLwEhScounZDqop+Dx
```
</details>
## Local development
### Building from source
> We recommend using a version manager as versions change very quickly. You will likely need to switch
between multiple Node.js versions based on the needs of different projects you're working on. [NVM on windows](https://github.com/coreybutler/nvm-windows#installation--upgrades) on Windows and [nvm](https://github.com/nvm-sh/nvm) on Linux/macOS are pretty good choices. Also recommended nodejs version is 16.15.0 LTS.
Execute the following commands to start a development server:
Execute the following commands to compile the app from its source code:
```sh
npm ci # Installs all dependencies
npm start # Serve a development version
```
To build the app:
```sh
npm run build # Compiles the app into the dist/ directory
```
You can then copy the files to a webserver's webroot of your choice.
To serve a development version of the app locally for testing, you need to use the command `npm start`.
### Running with Docker
This repository includes a Dockerfile, which builds the application from source and serves it with Nginx on port 80. To
use this locally, you can build the container like so:
```
docker build -t cinny:latest .
```
You can then run the container you've built with a command similar to this:
```
docker run -p 8080:80 cinny:latest
```
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by navigating to `http://localhost:8080`.
This will forward your `localhost` port 8080 to the container's port 80. You can visit the app in your browser by
navigating to `http://localhost:8080`.
Alternatively you can just pull the [DockerHub image](https://hub.docker.com/r/ajbura/cinny) by `docker pull ajbura/cinny`.
### Configuring default Homeserver
To set default Homeserver on login and register page, place a customized [`config.json`](config.json) in webroot of your choice.
## License
Copyright (c) 2021-present Ajay Bura (ajbura)
Copyright (c) 2021 Ajay Bura (ajbura)
Code licensed under the MIT License: <http://opensource.org/licenses/MIT>
+1203 -110
View File
File diff suppressed because it is too large. Load diff
+9 -4
View File
@@ -17,7 +17,6 @@
"dependencies": {
"@fontsource/inter": "^4.5.12",
"@fontsource/roboto": "^4.5.8",
"@khanacademy/simple-markdown": "^0.8.3",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.12.tgz",
"@tippyjs/react": "^4.2.6",
"babel-polyfill": "^6.26.0",
@@ -33,6 +32,12 @@
"linkify-html": "^4.0.0-beta.5",
"linkifyjs": "^4.0.0-beta.5",
"matrix-js-sdk": "^19.2.0",
"micromark": "^3.0.10",
"micromark-extension-gfm": "^2.0.1",
"micromark-extension-math": "^2.0.2",
"micromark-util-chunked": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-symbol": "^1.0.1",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-autosize-textarea": "^7.1.0",
@@ -59,7 +64,7 @@
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1",
"css-minimizer-webpack-plugin": "^4.0.0",
"eslint": "^8.22.0",
"eslint": "^8.21.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
@@ -71,7 +76,7 @@
"html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^2.6.1",
"path-browserify": "^1.0.1",
"sass": "^1.54.4",
"sass": "^1.54.3",
"sass-loader": "^13.0.2",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.1",
@@ -79,7 +84,7 @@
"util": "^0.12.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.10.0",
"webpack-dev-server": "^4.9.3",
"webpack-merge": "^5.7.3"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2021-present Ajay Bura (ajbura)
Graphics (c) by Ajay Bura (ajbura)
Graphic content is licensed under a
Creative Commons Attribution 4.0 International License.
@@ -38,7 +38,7 @@
.image-lightbox__content {
display: flex;
justify-content: center;
max-height: 80vh;
max-height: 90vh;
& img {
background-color: var(--bg-surface-low);
+8 -14
View File
@@ -714,9 +714,9 @@ function getEditedBody(editedMEvent) {
}
function Message({
mEvent, isBodyOnly, roomTimeline,
focus, fullTime, isEdit, setEdit, cancelEdit,
mEvent, isBodyOnly, roomTimeline, focus, fullTime,
}) {
const [isEditing, setIsEditing] = useState(false);
const roomId = mEvent.getRoomId();
const { editedTimeline, reactionTimeline } = roomTimeline ?? {};
@@ -731,7 +731,7 @@ function Message({
const avatarSrc = mEvent.sender?.getAvatarUrl(initMatrix.matrixClient.baseUrl, 36, 36, 'crop') ?? null;
const edit = useCallback(() => {
setEdit(eventId);
setIsEditing(true);
}, []);
const reply = useCallback(() => {
replyTo(senderId, mEvent.getId(), body);
@@ -788,7 +788,7 @@ function Message({
eventId={mEvent.replyEventId}
/>
)}
{!isEdit && (
{!isEditing && (
<MessageBody
senderName={username}
isCustomHTML={isCustomHTML}
@@ -797,22 +797,22 @@ function Message({
isEdited={isEdited}
/>
)}
{isEdit && (
{isEditing && (
<MessageEdit
body={body}
onSave={(newBody) => {
if (newBody !== body) {
initMatrix.roomsInput.sendEditedMessage(roomId, mEvent, newBody);
}
cancelEdit();
setIsEditing(false);
}}
onCancel={cancelEdit}
onCancel={() => setIsEditing(false)}
/>
)}
{haveReactions && (
<MessageReactionGroup roomTimeline={roomTimeline} mEvent={mEvent} />
)}
{roomTimeline && !isEdit && (
{roomTimeline && !isEditing && (
<MessageOptions
roomTimeline={roomTimeline}
mEvent={mEvent}
@@ -829,9 +829,6 @@ Message.defaultProps = {
focus: false,
roomTimeline: null,
fullTime: false,
isEdit: false,
setEdit: null,
cancelEdit: null,
};
Message.propTypes = {
mEvent: PropTypes.shape({}).isRequired,
@@ -839,9 +836,6 @@ Message.propTypes = {
roomTimeline: PropTypes.shape({}),
focus: PropTypes.bool,
fullTime: PropTypes.bool,
isEdit: PropTypes.bool,
setEdit: PropTypes.func,
cancelEdit: PropTypes.func,
};
export { Message, MessageReply, PlaceholderMessage };
+16 -33
View File
@@ -15,7 +15,7 @@
display: flex;
}
}
&__avatar-container {
padding-top: 6px;
@include dir.side(margin, 0, var(--sp-tight));
@@ -101,6 +101,7 @@
}
}
.message__header {
display: flex;
align-items: baseline;
@@ -114,16 +115,10 @@
@extend .cp-txt__ellipsis;
color: inherit;
}
& > span:last-child {
display: none;
}
& > span:last-child { display: none; }
&:hover {
& > span:first-child {
display: none;
}
& > span:last-child {
display: block;
}
& > span:first-child { display: none; }
& > span:last-child { display: block; }
}
}
@@ -173,11 +168,7 @@
& > .text > a {
white-space: initial !important;
}
& p:not(:last-child) {
margin-bottom: var(--sp-normal);
}
& span[data-mx-pill] {
background-color: hsla(0, 0%, 64%, 0.15);
padding: 0 2px;
@@ -200,7 +191,7 @@
& span[data-mx-spoiler] {
border-radius: 4px;
background-color: rgba(124, 124, 124, 0.5);
color: transparent;
color:transparent;
cursor: pointer;
-webkit-touch-callout: none;
-webkit-user-select: none;
@@ -229,8 +220,6 @@
padding: var(--sp-extra-tight) 0;
&-btns button {
margin: var(--sp-tight) 0 0 0;
padding: var(--sp-ultra-tight) var(--sp-tight);
min-width: 0;
@include dir.side(margin, 0, var(--sp-tight));
}
}
@@ -266,9 +255,9 @@
}
&-count {
margin: 0 var(--sp-ultra-tight);
color: var(--tc-surface-normal);
color: var(--tc-surface-normal)
}
&-tooltip .react-emoji {
&-tooltip .react-emoji {
width: 16px;
height: 16px;
margin: 0 var(--sp-ultra-tight);
@@ -281,19 +270,19 @@
}
}
&:active {
background-color: var(--bg-surface-active);
background-color: var(--bg-surface-active)
}
&--active {
background-color: var(--bg-caution-active);
@media (hover: hover) {
&:hover {
background-color: var(--bg-caution-hover);
}
}
&:active {
background-color: var(--bg-caution-active);
background-color: var(--bg-caution-active)
}
}
}
@@ -314,12 +303,7 @@
// markdown formating
.message__body {
& h1,
h2,
h3,
h4,
h5,
h6 {
& h1, h2, h3, h4, h5, h6 {
margin: 0;
margin-bottom: var(--sp-ultra-tight);
font-weight: var(--fw-medium);
@@ -440,8 +424,7 @@
@include scrollbar.scroll__h;
@include scrollbar.scroll--auto-hide;
& td,
& th {
& td, & th {
padding: var(--sp-extra-tight);
border: 1px solid var(--bg-surface-border);
border-width: 0 1px 1px 0;
@@ -449,11 +432,11 @@
&:last-child {
border-width: 0;
border-bottom-width: 1px;
[dir='rtl'] & {
[dir=rtl] & {
border-width: 0 1px 1px 0;
}
}
[dir='rtl'] &:first-child {
[dir=rtl] &:first-child {
border-width: 0;
border-bottom-width: 1px;
}
@@ -177,7 +177,7 @@ function getUserImagePack(mx) {
}
const userImagePack = ImagePack.parsePack(mx.getUserId(), accountDataEmoji.event.content);
if (userImagePack) userImagePack.displayName ??= 'Personal Emoji';
userImagePack.displayName ??= 'Personal Emoji';
return userImagePack;
}
+2 -59
View File
@@ -118,15 +118,7 @@ function handleOnClickCapture(e) {
}
}
function renderEvent(
roomTimeline,
mEvent,
prevMEvent,
isFocus,
isEdit,
setEdit,
cancelEdit,
) {
function renderEvent(roomTimeline, mEvent, prevMEvent, isFocus = false) {
const isBodyOnly = (prevMEvent !== null
&& prevMEvent.getSender() === mEvent.getSender()
&& prevMEvent.getType() !== 'm.room.member'
@@ -155,9 +147,6 @@ function renderEvent(
roomTimeline={roomTimeline}
focus={isFocus}
fullTime={false}
isEdit={isEdit}
setEdit={setEdit}
cancelEdit={cancelEdit}
/>
);
}
@@ -397,8 +386,6 @@ function RoomViewContent({ eventId, roomTimeline }) {
const timelineSVRef = useRef(null);
const timelineScrollRef = useRef(null);
const eventLimitRef = useRef(null);
const [editEventId, setEditEventId] = useState(null);
const cancelEdit = () => setEditEventId(null);
const readUptoEvtStore = useStore(roomTimeline);
const [onLimitUpdate, forceUpdateLimit] = useForceUpdate();
@@ -483,42 +470,6 @@ function RoomViewContent({ eventId, roomTimeline }) {
}
}, [newEvent]);
const listenKeyboard = useCallback((event) => {
if (event.ctrlKey || event.altKey || event.metaKey) return;
if (event.key !== 'ArrowUp') return;
if (navigation.isRawModalVisible) return;
if (document.activeElement.id !== 'message-textarea') return;
if (document.activeElement.value !== '') return;
const {
timeline: tl, activeTimeline, liveTimeline, matrixClient: mx,
} = roomTimeline;
const limit = eventLimitRef.current;
if (activeTimeline !== liveTimeline) return;
if (tl.length > limit.length) return;
const mTypes = ['m.text'];
for (let i = tl.length - 1; i >= 0; i -= 1) {
const mE = tl[i];
if (
mE.getSender() === mx.getUserId()
&& mE.getType() === 'm.room.message'
&& mTypes.includes(mE.getContent()?.msgtype)
) {
setEditEventId(mE.getId());
return;
}
}
}, [roomTimeline]);
useEffect(() => {
document.body.addEventListener('keydown', listenKeyboard);
return () => {
document.body.removeEventListener('keydown', listenKeyboard);
};
}, [listenKeyboard]);
const handleTimelineScroll = (event) => {
const timelineScroll = timelineScrollRef.current;
if (!event.target) return;
@@ -584,15 +535,7 @@ function RoomViewContent({ eventId, roomTimeline }) {
const isFocus = focusId === mEvent.getId();
if (isFocus) jumpToItemIndex = itemCountIndex;
tl.push(renderEvent(
roomTimeline,
mEvent,
isNewEvent ? null : prevMEvent,
isFocus,
editEventId === mEvent.getId(),
setEditEventId,
cancelEdit,
));
tl.push(renderEvent(roomTimeline, mEvent, isNewEvent ? null : prevMEvent, isFocus));
itemCountIndex += 1;
}
if (roomTimeline.canPaginateForward() || limit.length < timeline.length) {
+12 -6
View File
@@ -1,12 +1,15 @@
import EventEmitter from 'events';
import { micromark } from 'micromark';
import { gfm, gfmHtml } from 'micromark-extension-gfm';
import encrypt from 'browser-encrypt-attachment';
import { math } from 'micromark-extension-math';
import { encode } from 'blurhash';
import { getShortcodeToEmoji } from '../../app/organisms/emoji-board/custom-emoji';
import { mathExtensionHtml, spoilerExtension, spoilerExtensionHtml } from '../../util/markdown';
import { getBlobSafeMimeType } from '../../util/mimetypes';
import { sanitizeText } from '../../util/sanitize';
import cons from './cons';
import settings from './settings';
import { htmlOutput, parser } from '../../util/markdown';
const blurhashField = 'xyz.amorgan.blurhash';
const MXID_REGEX = /\B@\S+:\S+\.\S+[^.,:;?!\s]/g;
@@ -101,11 +104,14 @@ function getVideoThumbnail(video, width, height, mimeType) {
}
function getFormattedBody(markdown) {
let content = parser(markdown);
if (content.length === 1 && content[0].type === 'paragraph') {
content = content[0].content;
}
return htmlOutput(content);
const result = micromark(markdown, {
extensions: [gfm(), spoilerExtension(), math()],
htmlExtensions: [gfmHtml(), spoilerExtensionHtml, mathExtensionHtml],
});
const bodyParts = result.match(/^(<p>)(.*)(<\/p>)$/);
if (bodyParts === null) return result;
if (bodyParts[2].indexOf('</p>') >= 0) return result;
return bodyParts[2];
}
function getReplyFormattedBody(roomId, reply) {
+5 -13
View File
@@ -39,7 +39,7 @@ class Navigation extends EventEmitter {
}
_mapRoomToSpace(roomId) {
const { roomList, accountData } = this.initMatrix;
const { roomList } = this.initMatrix;
if (
this.selectedTab === cons.tabs.HOME
&& roomList.rooms.has(roomId)
@@ -61,21 +61,13 @@ class Navigation extends EventEmitter {
const parents = roomList.roomIdToParents.get(roomId);
if (!parents) return;
if (parents.has(this.selectedSpaceId)) {
this.spaceToRoom.set(this.selectedSpaceId, {
[...parents].forEach((pId) => {
this.spaceToRoom.set(pId, {
roomId,
timestamp: Date.now(),
});
} else if (accountData.categorizedSpaces.has(this.selectedSpaceId)) {
const categories = roomList.getCategorizedSpaces([this.selectedSpaceId]);
const parent = [...parents].find((pId) => categories.has(pId));
if (parent) {
this.spaceToRoom.set(parent, {
roomId,
timestamp: Date.now(),
});
}
}
});
}
_selectRoom(roomId, eventId) {
+187 -78
View File
@@ -1,89 +1,198 @@
import SimpleMarkdown from '@khanacademy/simple-markdown';
/* eslint-disable no-param-reassign */
/* eslint-disable no-plusplus */
/* eslint-disable no-continue */
const {
defaultRules, parserFor, outputFor, anyScopeRegex, blockRegex, inlineRegex, htmlTag, sanitizeText,
} = SimpleMarkdown;
import { codes } from 'micromark-util-symbol/codes';
import { types } from 'micromark-util-symbol/types';
import { resolveAll } from 'micromark-util-resolve-all';
import { splice } from 'micromark-util-chunked';
function mathHtml(wrap, node) {
return htmlTag(wrap, htmlTag('code', sanitizeText(node.content)), { 'data-mx-maths': node.content });
function inlineExtension(marker, len, key) {
const keySeq = `${key}Sequence`;
const keySeqTmp = `${keySeq}Temporary`;
return () => {
function tokenize(effects, ok, nok) {
const { previous, events } = this;
let size = 0;
function more(code) {
// consume more markers if the maximum length hasn't been reached yet
if (code === marker && size < len) {
effects.consume(code);
size += 1;
return more;
}
// check for minimum length
if (size < len) return nok(code);
effects.exit(keySeqTmp);
return ok(code);
}
function start(code) {
// ignore code if it's not a marker
if (code !== marker) return nok(code);
if (previous === marker
&& events[events.length - 1][1].type !== types.characterEscape) return nok(code);
effects.enter(keySeqTmp);
return more(code);
}
return start;
}
function resolve(events, context) {
let i = -1;
while (++i < events.length) {
if (events[i][0] !== 'enter' || events[i][1].type !== keySeqTmp) continue;
let open = i;
while (open--) {
if (events[open][0] !== 'exit' || events[open][1].type !== keySeqTmp) continue;
events[i][1].type = keySeq;
events[open][1].type = keySeq;
const border = {
type: key,
start: { ...events[open][1].start },
end: { ...events[i][1].end },
};
const text = {
type: `${key}Text`,
start: { ...events[open][1].end },
end: { ...events[i][1].start },
};
const nextEvents = [
['enter', border, context],
['enter', events[open][1], context],
['exit', events[open][1], context],
['enter', text, context],
];
splice(
nextEvents,
nextEvents.length,
0,
resolveAll(
context.parser.constructs.insideSpan.null,
events.slice(open + 1, i),
context,
),
);
splice(nextEvents, nextEvents.length, 0, [
['exit', text, context],
['enter', events[i][1], context],
['exit', events[i][1], context],
['exit', border, context],
]);
splice(events, open - 1, i - open + 3, nextEvents);
i = open + nextEvents.length - 2;
break;
}
}
events.forEach((event) => {
if (event[1].type === keySeqTmp) {
event[1].type = types.data;
}
});
return events;
}
const tokenizer = {
tokenize,
resolveAll: resolve,
};
return {
text: { [marker]: tokenizer },
insideSpan: { null: [tokenizer] },
attentionMarkers: { null: [marker] },
};
};
}
const rules = {
...defaultRules,
Array: {
...defaultRules.Array,
plain: (arr, output, state) => arr.map((node) => output(node, state)).join(''),
const spoilerExtension = inlineExtension(codes.verticalBar, 2, 'spoiler');
const spoilerExtensionHtml = {
enter: {
spoiler() {
this.tag('<span data-mx-spoiler>');
},
},
displayMath: {
order: defaultRules.list.order + 0.5,
match: blockRegex(/^\$\$\n*([\s\S]+?)\n*\$\$/),
parse: (capture) => ({ content: capture[1] }),
plain: (node) => `$$\n${node.content}\n$$`,
html: (node) => mathHtml('div', node),
},
newline: {
...defaultRules.newline,
plain: () => '\n',
},
paragraph: {
...defaultRules.paragraph,
plain: (node, output, state) => `${output(node.content, state)}\n\n`,
html: (node, output, state) => htmlTag('p', output(node.content, state)),
},
escape: {
...defaultRules.escape,
plain: (node, output, state) => `\\${output(node.content, state)}`,
},
em: {
...defaultRules.em,
plain: (node, output, state) => `_${output(node.content, state)}_`,
},
strong: {
...defaultRules.strong,
plain: (node, output, state) => `**${output(node.content, state)}**`,
},
u: {
...defaultRules.u,
plain: (node, output, state) => `__${output(node.content, state)}__`,
},
del: {
...defaultRules.del,
plain: (node, output, state) => `~~${output(node.content, state)}~~`,
},
spoiler: {
order: defaultRules.em.order - 0.5,
match: inlineRegex(/^\|\|([\s\S]+?)\|\|(?:\(([\s\S]+?)\))?/),
parse: (capture, parse, state) => ({
content: parse(capture[1], state),
reason: capture[2],
}),
plain: (node) => `[spoiler${node.reason ? `: ${node.reason}` : ''}](mxc://somewhere)`,
html: (node, output, state) => `<span data-mx-spoiler${node.reason ? `="${sanitizeText(node.reason)}"` : ''}>${output(node.content, state)}</span>`,
},
inlineMath: {
order: defaultRules.del.order + 0.5,
match: inlineRegex(/^\$(\S[\s\S]+?\S|\S)\$(?!\d)/),
parse: (capture) => ({ content: capture[1] }),
plain: (node) => `$${node.content}$`,
html: (node) => mathHtml('span', node),
},
br: {
...defaultRules.br,
match: anyScopeRegex(/^ *\n/),
plain: () => '\n',
},
text: {
...defaultRules.text,
match: anyScopeRegex(/^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff]| *\n|\w+:\S|$)/),
plain: (node) => node.content,
exit: {
spoiler() {
this.tag('</span>');
},
},
};
const parser = parserFor(rules);
const plainOutput = outputFor(rules, 'plain');
const htmlOutput = outputFor(rules, 'html');
const mathExtensionHtml = {
enter: {
mathFlow() {
this.lineEndingIfNeeded();
},
mathFlowFenceMeta() {
this.buffer();
},
mathText() {
this.buffer();
},
},
exit: {
mathFlow() {
const value = this.encode(this.resume().replace(/(?:\r?\n|\r)$/, ''));
this.tag('<div data-mx-maths="');
this.tag(value);
this.tag('"><code>');
this.raw(value);
this.tag('</code></div>');
this.setData('mathFlowOpen');
this.setData('slurpOneLineEnding');
},
mathFlowFence() {
// After the first fence.
if (!this.getData('mathFlowOpen')) {
this.setData('mathFlowOpen', true);
this.setData('slurpOneLineEnding', true);
this.buffer();
}
},
mathFlowFenceMeta() {
this.resume();
},
mathFlowValue(token) {
this.raw(this.sliceSerialize(token));
},
mathText() {
const value = this.encode(this.resume());
this.tag('<span data-mx-maths="');
this.tag(value);
this.tag('"><code>');
this.raw(value);
this.tag('</code></span>');
},
mathTextData(token) {
this.raw(this.sliceSerialize(token));
},
},
};
export {
parser, plainOutput, htmlOutput,
inlineExtension,
spoilerExtension, spoilerExtensionHtml,
mathExtensionHtml,
};
+1 -1
View File
@@ -19,7 +19,7 @@ const permittedTagToAttributes = {
div: ['data-mx-maths'],
a: ['name', 'target', 'href', 'rel'],
img: ['width', 'height', 'alt', 'title', 'src', 'data-mx-emoticon'],
ol: ['start'],
o: ['start'],
code: ['class'],
};