merging files
Этот коммит содержится в:
1112
webapp/utils/async_client.jsx
Обычный файл
1112
webapp/utils/async_client.jsx
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
254
webapp/utils/channel_intro_messages.jsx
Обычный файл
254
webapp/utils/channel_intro_messages.jsx
Обычный файл
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from './utils.jsx';
|
||||
import ChannelInviteModal from 'components/channel_invite_modal.jsx';
|
||||
import EditChannelHeaderModal from 'components/edit_channel_header_modal.jsx';
|
||||
import ToggleModalButton from 'components/toggle_modal_button.jsx';
|
||||
import UserProfile from 'components/user_profile.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
|
||||
|
||||
export function createChannelIntroMessage(channel) {
|
||||
if (channel.type === 'D') {
|
||||
return createDMIntroMessage(channel);
|
||||
} else if (ChannelStore.isDefault(channel)) {
|
||||
return createDefaultIntroMessage(channel);
|
||||
} else if (channel.name === Constants.OFFTOPIC_CHANNEL) {
|
||||
return createOffTopicIntroMessage(channel);
|
||||
} else if (channel.type === 'O' || channel.type === 'P') {
|
||||
return createStandardIntroMessage(channel);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createDMIntroMessage(channel) {
|
||||
var teammate = Utils.getDirectTeammate(channel.id);
|
||||
|
||||
if (teammate) {
|
||||
var teammateName = teammate.username;
|
||||
if (teammate.nickname.length > 0) {
|
||||
teammateName = teammate.nickname;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<div className='post-profile-img__container channel-intro-img'>
|
||||
<img
|
||||
className='post-profile-img'
|
||||
src={'/api/v1/users/' + teammate.id + '/image?time=' + teammate.update_at}
|
||||
height='50'
|
||||
width='50'
|
||||
/>
|
||||
</div>
|
||||
<div className='channel-intro-profile'>
|
||||
<strong>
|
||||
<UserProfile user={teammate}/>
|
||||
</strong>
|
||||
</div>
|
||||
<p className='channel-intro-text'>
|
||||
<FormattedHTMLMessage
|
||||
id='intro_messages.DM'
|
||||
defaultMessage='This is the start of your direct message history with {teammate}.<br />Direct messages and files shared here are not shown to people outside this area.'
|
||||
values={{
|
||||
teammate: teammateName
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{createSetHeaderButton(channel)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<p className='channel-intro-text'>
|
||||
<FormattedMessage
|
||||
id='intro_messages.teammate'
|
||||
defaultMessage='This is the start of your direct message history with this teammate. Direct messages and files shared here are not shown to people outside this area.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createOffTopicIntroMessage(channel) {
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<FormattedHTMLMessage
|
||||
id='intro_messages.offTopic'
|
||||
defaultMessage='<h4 class="channel-intro__title">Beginning of {display_name}</h4><p class="channel-intro__content">This is the start of {display_name}, a channel for non-work-related conversations.<br/></p>'
|
||||
values={{
|
||||
display_name: channel.display_name
|
||||
}}
|
||||
/>
|
||||
{createSetHeaderButton(channel)}
|
||||
{createInviteChannelMemberButton(channel, 'channel')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createDefaultIntroMessage(channel) {
|
||||
const inviteModalLink = (
|
||||
<a
|
||||
className='intro-links'
|
||||
href='#'
|
||||
onClick={GlobalActions.showGetTeamInviteLinkModal}
|
||||
>
|
||||
<i className='fa fa-user-plus'></i>
|
||||
<FormattedMessage
|
||||
id='intro_messages.inviteOthers'
|
||||
defaultMessage='Invite others to this team'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<FormattedHTMLMessage
|
||||
id='intro_messages.default'
|
||||
defaultMessage="<h4 class='channel-intro__title'>Beginning of {display_name}</h4><p class='channel-intro__content'><strong>Welcome to {display_name}!</strong><br/><br/>This is the first channel teammates see when they sign up - use it for posting updates everyone needs to know.</p>"
|
||||
values={{
|
||||
display_name: channel.display_name
|
||||
}}
|
||||
/>
|
||||
{inviteModalLink}
|
||||
{createSetHeaderButton(channel)}
|
||||
<br/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function createStandardIntroMessage(channel) {
|
||||
var uiName = channel.display_name;
|
||||
var creatorName = '';
|
||||
|
||||
var uiType;
|
||||
var memberMessage;
|
||||
if (channel.type === 'P') {
|
||||
uiType = (
|
||||
<FormattedMessage
|
||||
id='intro_messages.group'
|
||||
defaultMessage='private group'
|
||||
/>
|
||||
);
|
||||
memberMessage = (
|
||||
<FormattedMessage
|
||||
id='intro_messages.onlyInvited'
|
||||
defaultMessage=' Only invited members can see this private group.'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
uiType = (
|
||||
<FormattedMessage
|
||||
id='intro_messages.channel'
|
||||
defaultMessage='channel'
|
||||
/>
|
||||
);
|
||||
memberMessage = (
|
||||
<FormattedMessage
|
||||
id='intro_messages.anyMember'
|
||||
defaultMessage=' Any member can join and read this channel.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const date = (
|
||||
<FormattedDate
|
||||
value={channel.create_at}
|
||||
month='long'
|
||||
day='2-digit'
|
||||
year='numeric'
|
||||
/>
|
||||
);
|
||||
|
||||
var createMessage;
|
||||
if (creatorName === '') {
|
||||
createMessage = (
|
||||
<FormattedMessage
|
||||
id='intro_messages.noCreator'
|
||||
defaultMessage='This is the start of the {name} {type}, created on {date}.'
|
||||
values={{
|
||||
name: (uiName),
|
||||
type: (uiType),
|
||||
date: (date)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
createMessage = (
|
||||
<span>
|
||||
<FormattedHTMLMessage
|
||||
id='intro_messages.creator'
|
||||
defaultMessage='This is the start of the <strong>{name}</strong> {type}, created by <strong>{creator}</strong> on <strong>{date}</strong>'
|
||||
values={{
|
||||
name: (uiName),
|
||||
type: (uiType),
|
||||
date: (date),
|
||||
creator: creatorName
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<h4 className='channel-intro__title'>
|
||||
<FormattedMessage
|
||||
id='intro_messages.beginning'
|
||||
defaultMessage='Beginning of {name}'
|
||||
values={{
|
||||
name: (uiName)
|
||||
}}
|
||||
/>
|
||||
</h4>
|
||||
<p className='channel-intro__content'>
|
||||
{createMessage}
|
||||
{memberMessage}
|
||||
<br/>
|
||||
</p>
|
||||
{createSetHeaderButton(channel)}
|
||||
{createInviteChannelMemberButton(channel, uiType)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function createInviteChannelMemberButton(channel, uiType) {
|
||||
return (
|
||||
<ToggleModalButton
|
||||
className='intro-links'
|
||||
dialogType={ChannelInviteModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<i className='fa fa-user-plus'></i>
|
||||
<FormattedMessage
|
||||
id='intro_messages.invite'
|
||||
defaultMessage='Invite others to this {type}'
|
||||
values={{
|
||||
type: (uiType)
|
||||
}}
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
);
|
||||
}
|
||||
|
||||
function createSetHeaderButton(channel) {
|
||||
return (
|
||||
<ToggleModalButton
|
||||
className='intro-links'
|
||||
dialogType={EditChannelHeaderModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<i className='fa fa-pencil'></i>
|
||||
<FormattedMessage
|
||||
id='intro_messages.setHeader'
|
||||
defaultMessage='Set a Header'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
);
|
||||
}
|
||||
1680
webapp/utils/client.jsx
Обычный файл
1680
webapp/utils/client.jsx
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
573
webapp/utils/constants.jsx
Обычный файл
573
webapp/utils/constants.jsx
Обычный файл
@@ -0,0 +1,573 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import keyMirror from 'keymirror';
|
||||
|
||||
import audioIcon from 'images/icons/audio.png';
|
||||
import videoIcon from 'images/icons/video.png';
|
||||
import excelIcon from 'images/icons/excel.png';
|
||||
import pptIcon from 'images/icons/ppt.png';
|
||||
import pdfIcon from 'images/icons/pdf.png';
|
||||
import codeIcon from 'images/icons/code.png';
|
||||
import wordIcon from 'images/icons/word.png';
|
||||
import patchIcon from 'images/icons/patch.png';
|
||||
import genericIcon from 'images/icons/generic.png';
|
||||
|
||||
import logoImage from 'images/logo_compact.png';
|
||||
|
||||
import solarizedDarkCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/solarized-dark.css';
|
||||
import solarizedDarkIcon from 'images/themes/code_themes/solarized-dark.png';
|
||||
|
||||
import solarizedLightCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/solarized-light.css';
|
||||
import solarizedLightIcon from 'images/themes/code_themes/solarized-light.png';
|
||||
|
||||
import githubCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/github.css';
|
||||
import githubIcon from 'images/themes/code_themes/github.png';
|
||||
|
||||
import monokaiCSS from '!!file?name=files/code_themes/[hash].[ext]!highlight.js/styles/monokai.css';
|
||||
import monokaiIcon from 'images/themes/code_themes/monokai.png';
|
||||
|
||||
import defaultThemeImage from 'images/themes/organization.png';
|
||||
import mattermostDarkThemeImage from 'images/themes/mattermost_dark.png';
|
||||
import mattermostThemeImage from 'images/themes/mattermost.png';
|
||||
import windows10ThemeImage from 'images/themes/windows_dark.png';
|
||||
|
||||
export default {
|
||||
ActionTypes: keyMirror({
|
||||
RECEIVED_ERROR: null,
|
||||
|
||||
CLICK_CHANNEL: null,
|
||||
CREATE_CHANNEL: null,
|
||||
LEAVE_CHANNEL: null,
|
||||
CREATE_POST: null,
|
||||
POST_DELETED: null,
|
||||
REMOVE_POST: null,
|
||||
|
||||
RECEIVED_CHANNELS: null,
|
||||
RECEIVED_CHANNEL: null,
|
||||
RECEIVED_MORE_CHANNELS: null,
|
||||
RECEIVED_CHANNEL_EXTRA_INFO: null,
|
||||
|
||||
FOCUS_POST: null,
|
||||
RECEIVED_POSTS: null,
|
||||
RECEIVED_FOCUSED_POST: null,
|
||||
RECEIVED_POST: null,
|
||||
RECEIVED_EDIT_POST: null,
|
||||
RECEIVED_SEARCH: null,
|
||||
RECEIVED_SEARCH_TERM: null,
|
||||
RECEIVED_POST_SELECTED: null,
|
||||
RECEIVED_MENTION_DATA: null,
|
||||
RECEIVED_ADD_MENTION: null,
|
||||
|
||||
RECEIVED_PROFILES: null,
|
||||
RECEIVED_ME: null,
|
||||
RECEIVED_SESSIONS: null,
|
||||
RECEIVED_AUDITS: null,
|
||||
RECEIVED_TEAMS: null,
|
||||
RECEIVED_STATUSES: null,
|
||||
RECEIVED_PREFERENCE: null,
|
||||
RECEIVED_PREFERENCES: null,
|
||||
RECEIVED_FILE_INFO: null,
|
||||
|
||||
RECEIVED_MSG: null,
|
||||
|
||||
RECEIVED_MY_TEAM: null,
|
||||
|
||||
RECEIVED_CONFIG: null,
|
||||
RECEIVED_LOGS: null,
|
||||
RECEIVED_SERVER_AUDITS: null,
|
||||
RECEIVED_SERVER_COMPLIANCE_REPORTS: null,
|
||||
RECEIVED_ALL_TEAMS: null,
|
||||
|
||||
RECEIVED_LOCALE: null,
|
||||
|
||||
SHOW_SEARCH: null,
|
||||
|
||||
TOGGLE_IMPORT_THEME_MODAL: null,
|
||||
TOGGLE_INVITE_MEMBER_MODAL: null,
|
||||
TOGGLE_DELETE_POST_MODAL: null,
|
||||
TOGGLE_GET_POST_LINK_MODAL: null,
|
||||
TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
|
||||
TOGGLE_REGISTER_APP_MODAL: null,
|
||||
|
||||
SUGGESTION_PRETEXT_CHANGED: null,
|
||||
SUGGESTION_RECEIVED_SUGGESTIONS: null,
|
||||
SUGGESTION_CLEAR_SUGGESTIONS: null,
|
||||
SUGGESTION_COMPLETE_WORD: null,
|
||||
SUGGESTION_SELECT_NEXT: null,
|
||||
SUGGESTION_SELECT_PREVIOUS: null
|
||||
}),
|
||||
|
||||
PayloadSources: keyMirror({
|
||||
SERVER_ACTION: null,
|
||||
VIEW_ACTION: null
|
||||
}),
|
||||
|
||||
StatTypes: keyMirror({
|
||||
TOTAL_USERS: null,
|
||||
TOTAL_PUBLIC_CHANNELS: null,
|
||||
TOTAL_PRIVATE_GROUPS: null,
|
||||
TOTAL_POSTS: null,
|
||||
TOTAL_TEAMS: null,
|
||||
TOTAL_FILE_POSTS: null,
|
||||
TOTAL_HASHTAG_POSTS: null,
|
||||
TOTAL_IHOOKS: null,
|
||||
TOTAL_OHOOKS: null,
|
||||
TOTAL_COMMANDS: null,
|
||||
TOTAL_SESSIONS: null,
|
||||
POST_PER_DAY: null,
|
||||
USERS_WITH_POSTS_PER_DAY: null,
|
||||
RECENTLY_ACTIVE_USERS: null,
|
||||
NEWLY_CREATED_USERS: null
|
||||
}),
|
||||
STAT_MAX_ACTIVE_USERS: 20,
|
||||
STAT_MAX_NEW_USERS: 20,
|
||||
|
||||
SocketEvents: {
|
||||
POSTED: 'posted',
|
||||
POST_EDITED: 'post_edited',
|
||||
POST_DELETED: 'post_deleted',
|
||||
CHANNEL_VIEWED: 'channel_viewed',
|
||||
NEW_USER: 'new_user',
|
||||
USER_ADDED: 'user_added',
|
||||
USER_REMOVED: 'user_removed',
|
||||
TYPING: 'typing',
|
||||
PREFERENCE_CHANGED: 'preference_changed',
|
||||
EPHEMERAL_MESSAGE: 'ephemeral_message'
|
||||
},
|
||||
|
||||
//SPECIAL_MENTIONS: ['all', 'channel'],
|
||||
SPECIAL_MENTIONS: ['channel'],
|
||||
CHARACTER_LIMIT: 4000,
|
||||
IMAGE_TYPES: ['jpg', 'gif', 'bmp', 'png', 'jpeg'],
|
||||
AUDIO_TYPES: ['mp3', 'wav', 'wma', 'm4a', 'flac', 'aac', 'ogg'],
|
||||
VIDEO_TYPES: ['mp4', 'avi', 'webm', 'mkv', 'wmv', 'mpg', 'mov', 'flv'],
|
||||
PRESENTATION_TYPES: ['ppt', 'pptx'],
|
||||
SPREADSHEET_TYPES: ['xlsx', 'csv'],
|
||||
WORD_TYPES: ['doc', 'docx'],
|
||||
CODE_TYPES: ['css', 'html', 'js', 'php', 'rb'],
|
||||
PDF_TYPES: ['pdf'],
|
||||
PATCH_TYPES: ['patch'],
|
||||
ICON_FROM_TYPE: {
|
||||
audio: audioIcon,
|
||||
video: videoIcon,
|
||||
spreadsheet: excelIcon,
|
||||
presentation: pptIcon,
|
||||
pdf: pdfIcon,
|
||||
code: codeIcon,
|
||||
word: wordIcon,
|
||||
patch: patchIcon,
|
||||
other: genericIcon
|
||||
},
|
||||
ICON_NAME_FROM_TYPE: {
|
||||
audio: 'audio',
|
||||
video: 'video',
|
||||
spreadsheet: 'excel',
|
||||
presentation: 'ppt',
|
||||
pdf: 'pdf',
|
||||
code: 'code',
|
||||
word: 'word',
|
||||
patch: 'patch',
|
||||
other: 'generic'
|
||||
},
|
||||
MAX_DISPLAY_FILES: 5,
|
||||
MAX_UPLOAD_FILES: 5,
|
||||
MAX_FILE_SIZE: 50000000, // 50 MB
|
||||
THUMBNAIL_WIDTH: 128,
|
||||
THUMBNAIL_HEIGHT: 100,
|
||||
WEB_VIDEO_WIDTH: 640,
|
||||
WEB_VIDEO_HEIGHT: 480,
|
||||
MOBILE_VIDEO_WIDTH: 480,
|
||||
MOBILE_VIDEO_HEIGHT: 360,
|
||||
DEFAULT_CHANNEL: 'town-square',
|
||||
OFFTOPIC_CHANNEL: 'off-topic',
|
||||
GITLAB_SERVICE: 'gitlab',
|
||||
GOOGLE_SERVICE: 'google',
|
||||
EMAIL_SERVICE: 'email',
|
||||
SIGNIN_CHANGE: 'signin_change',
|
||||
SIGNIN_VERIFIED: 'verified',
|
||||
SESSION_EXPIRED: 'expired',
|
||||
POST_CHUNK_SIZE: 60,
|
||||
MAX_POST_CHUNKS: 3,
|
||||
POST_FOCUS_CONTEXT_RADIUS: 10,
|
||||
POST_LOADING: 'loading',
|
||||
POST_FAILED: 'failed',
|
||||
POST_DELETED: 'deleted',
|
||||
POST_TYPE_EPHEMERAL: 'system_ephemeral',
|
||||
POST_TYPE_JOIN_LEAVE: 'system_join_leave',
|
||||
SYSTEM_MESSAGE_PREFIX: 'system_',
|
||||
SYSTEM_MESSAGE_PROFILE_NAME: 'System',
|
||||
SYSTEM_MESSAGE_PROFILE_IMAGE: logoImage,
|
||||
RESERVED_TEAM_NAMES: [
|
||||
'www',
|
||||
'web',
|
||||
'admin',
|
||||
'support',
|
||||
'notify',
|
||||
'test',
|
||||
'demo',
|
||||
'mail',
|
||||
'team',
|
||||
'channel',
|
||||
'internal',
|
||||
'localhost',
|
||||
'dockerhost',
|
||||
'stag',
|
||||
'post',
|
||||
'cluster',
|
||||
'api'
|
||||
],
|
||||
RESERVED_USERNAMES: [
|
||||
'valet',
|
||||
'all',
|
||||
'channel'
|
||||
],
|
||||
MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
MAX_DMS: 20,
|
||||
MAX_CHANNEL_POPOVER_COUNT: 100,
|
||||
DM_CHANNEL: 'D',
|
||||
OPEN_CHANNEL: 'O',
|
||||
PRIVATE_CHANNEL: 'P',
|
||||
INVITE_TEAM: 'I',
|
||||
OPEN_TEAM: 'O',
|
||||
MAX_POST_LEN: 4000,
|
||||
EMOJI_SIZE: 16,
|
||||
ONLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-243 245 12 12'style='enable-background:new -243 245 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <path class='online--icon' d='M-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5C-236,250.5-236,250.5-236,250.5z'/> <ellipse class='online--icon' cx='-238.5' cy='248' rx='2.5' ry='2.5'/> </g> <path class='online--icon' d='M-238.9,253.8c0-0.4,0.1-0.9,0.2-1.3c-2.2-0.2-2.2-2-2.2-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5c0,0.1-0.1,0.5,0,0.6 c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0C-238.7,255.7-238.9,254.8-238.9,253.8z'/> <g> <g> <path class='online--icon' d='M-232.3,250.1l1.3,1.3c0,0,0,0.1,0,0.1l-4.1,4.1c0,0,0,0-0.1,0c0,0,0,0,0,0l-2.7-2.7c0,0,0-0.1,0-0.1l1.2-1.2 c0,0,0.1,0,0.1,0l1.4,1.4l2.9-2.9C-232.4,250.1-232.3,250.1-232.3,250.1z'/> </g> </g> </svg>",
|
||||
AWAY_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <ellipse class='away--icon' cx='-294.6' cy='394' rx='2.5' ry='2.5'/> <path class='away--icon' d='M-293.8,399.4c0-0.4,0.1-0.7,0.2-1c-0.3,0.1-0.6,0.2-1,0.2c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0c0.7,0,1.4-0.1,2-0.3 C-293.3,401.5-293.8,400.5-293.8,399.4z'/> </g> <path class='away--icon' d='M-287,400c0,0.1-0.1,0.1-0.1,0.1l-4.9,0c-0.1,0-0.1-0.1-0.1-0.1v-1.6c0-0.1,0.1-0.1,0.1-0.1l4.9,0c0.1,0,0.1,0.1,0.1,0.1 V400z'/> </svg>",
|
||||
OFFLINE_ICON_SVG: "<svg version='1.1'id='Layer_1' xmlns:dc='http://purl.org/dc/elements/1.1/' xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:svg='http://www.w3.org/2000/svg' xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:cc='http://creativecommons.org/ns#' inkscape:version='0.48.4 r9939' sodipodi:docname='TRASH_1_4.svg'xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='-299 391 12 12'style='enable-background:new -299 391 12 12;' xml:space='preserve'> <sodipodi:namedview inkscape:cx='26.358185' inkscape:zoom='1.18' bordercolor='#666666' pagecolor='#ffffff' borderopacity='1' objecttolerance='10' inkscape:cy='139.7898' gridtolerance='10' guidetolerance='10' showgrid='false' showguides='true' id='namedview6' inkscape:pageopacity='0' inkscape:pageshadow='2' inkscape:guide-bbox='true' inkscape:window-width='1366' inkscape:current-layer='Layer_1' inkscape:window-height='705' inkscape:window-y='-8' inkscape:window-maximized='1' inkscape:window-x='-8'> <sodipodi:guide position='50.036793,85.991376' orientation='1,0' id='guide2986'></sodipodi:guide> <sodipodi:guide position='58.426196,66.216355' orientation='0,1' id='guide3047'></sodipodi:guide> </sodipodi:namedview> <g> <g> <ellipse class='offline--icon' cx='-294.5' cy='394' rx='2.5' ry='2.5'/> <path class='offline--icon' d='M-294.3,399.7c0-0.4,0.1-0.8,0.2-1.2c-0.1,0-0.2,0-0.4,0c-2.5,0-2.5-2-2.5-2s-1,0.1-1.2,0.5c-0.4,0.6-0.6,1.7-0.7,2.5 c0,0.1-0.1,0.5,0,0.6c0.2,1.3,2.2,2.3,4.4,2.4h0.1h0.1c0.3,0,0.7,0,1-0.1C-293.9,401.6-294.3,400.7-294.3,399.7z'/> </g> </g> <g> <path class='offline--icon' d='M-288.9,399.4l1.8-1.8c0.1-0.1,0.1-0.3,0-0.3l-0.7-0.7c-0.1-0.1-0.3-0.1-0.3,0l-1.8,1.8l-1.8-1.8c-0.1-0.1-0.3-0.1-0.3,0 l-0.7,0.7c-0.1,0.1-0.1,0.3,0,0.3l1.8,1.8l-1.8,1.8c-0.1,0.1-0.1,0.3,0,0.3l0.7,0.7c0.1,0.1,0.3,0.1,0.3,0l1.8-1.8l1.8,1.8 c0.1,0.1,0.3,0.1,0.3,0l0.7-0.7c0.1-0.1,0.1-0.3,0-0.3L-288.9,399.4z'/> </g> </svg>",
|
||||
MENU_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='4px' height='16px' viewBox='0 0 8 32' enable-background='new 0 0 8 32' xml:space='preserve'> <g> <circle cx='4' cy='4.062' r='4'/> <circle cx='4' cy='16' r='4'/> <circle cx='4' cy='28' r='4'/> </g> </svg>",
|
||||
COMMENT_ICON: "<svg version='1.1' id='Layer_2' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'width='15px' height='15px' viewBox='1 1.5 15 15' enable-background='new 1 1.5 15 15' xml:space='preserve'> <g> <g> <path fill='#211B1B' d='M14,1.5H3c-1.104,0-2,0.896-2,2v8c0,1.104,0.896,2,2,2h1.628l1.884,3l1.866-3H14c1.104,0,2-0.896,2-2v-8 C16,2.396,15.104,1.5,14,1.5z M15,11.5c0,0.553-0.447,1-1,1H8l-1.493,2l-1.504-1.991L5,12.5H3c-0.552,0-1-0.447-1-1v-8 c0-0.552,0.448-1,1-1h11c0.553,0,1,0.448,1,1V11.5z'/> </g> </g> </svg>",
|
||||
REPLY_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-158 242 18 18' style='enable-background:new -158 242 18 18;' xml:space='preserve'> <path d='M-142.2,252.6c-2-3-4.8-4.7-8.3-4.8v-3.3c0-0.2-0.1-0.3-0.2-0.3s-0.3,0-0.4,0.1l-6.9,6.2c-0.1,0.1-0.1,0.2-0.1,0.3 c0,0.1,0,0.2,0.1,0.3l6.9,6.4c0.1,0.1,0.3,0.1,0.4,0.1c0.1-0.1,0.2-0.2,0.2-0.4v-3.8c4.2,0,7.4,0.4,9.6,4.4c0.1,0.1,0.2,0.2,0.3,0.2 c0,0,0.1,0,0.1,0c0.2-0.1,0.3-0.3,0.2-0.4C-140.2,257.3-140.6,255-142.2,252.6z M-150.8,252.5c-0.2,0-0.4,0.2-0.4,0.4v3.3l-6-5.5 l6-5.3v2.8c0,0.2,0.2,0.4,0.4,0.4c3.3,0,6,1.5,8,4.5c0.5,0.8,0.9,1.6,1.2,2.3C-144,252.8-147.1,252.5-150.8,252.5z'/> </svg>",
|
||||
SCROLL_BOTTOM_ICON: "<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px'viewBox='-239 239 21 23' style='enable-background:new -239 239 21 23;' xml:space='preserve'> <path d='M-239,241.4l2.4-2.4l8.1,8.2l8.1-8.2l2.4,2.4l-10.5,10.6L-239,241.4z M-228.5,257.2l8.1-8.2l2.4,2.4l-10.5,10.6l-10.5-10.6 l2.4-2.4L-228.5,257.2z'/> </svg>",
|
||||
UPDATE_TYPING_MS: 5000,
|
||||
THEMES: {
|
||||
default: {
|
||||
type: 'Organization',
|
||||
sidebarBg: '#2071a7',
|
||||
sidebarText: '#fff',
|
||||
sidebarUnreadText: '#fff',
|
||||
sidebarTextHoverBg: '#136197',
|
||||
sidebarTextActiveBorder: '#7AB0D6',
|
||||
sidebarTextActiveColor: '#FFFFFF',
|
||||
sidebarHeaderBg: '#2f81b7',
|
||||
sidebarHeaderTextColor: '#FFFFFF',
|
||||
onlineIndicator: '#7DBE00',
|
||||
awayIndicator: '#DCBD4E',
|
||||
mentionBj: '#FBFBFB',
|
||||
mentionColor: '#2071A7',
|
||||
centerChannelBg: '#f2f4f8',
|
||||
centerChannelColor: '#333333',
|
||||
newMessageSeparator: '#FF8800',
|
||||
linkColor: '#2f81b7',
|
||||
buttonBg: '#1dacfc',
|
||||
buttonColor: '#FFFFFF',
|
||||
mentionHighlightBg: '#fff2bb',
|
||||
mentionHighlightLink: '#2f81b7',
|
||||
codeTheme: 'github',
|
||||
image: defaultThemeImage
|
||||
},
|
||||
mattermost: {
|
||||
type: 'Mattermost',
|
||||
sidebarBg: '#fafafa',
|
||||
sidebarText: '#333333',
|
||||
sidebarUnreadText: '#333333',
|
||||
sidebarTextHoverBg: '#e6f2fa',
|
||||
sidebarTextActiveBorder: '#378FD2',
|
||||
sidebarTextActiveColor: '#111111',
|
||||
sidebarHeaderBg: '#2389d7',
|
||||
sidebarHeaderTextColor: '#ffffff',
|
||||
onlineIndicator: '#7DBE00',
|
||||
awayIndicator: '#DCBD4E',
|
||||
mentionBj: '#2389d7',
|
||||
mentionColor: '#ffffff',
|
||||
centerChannelBg: '#ffffff',
|
||||
centerChannelColor: '#333333',
|
||||
newMessageSeparator: '#FF8800',
|
||||
linkColor: '#2389d7',
|
||||
buttonBg: '#2389d7',
|
||||
buttonColor: '#FFFFFF',
|
||||
mentionHighlightBg: '#fff2bb',
|
||||
mentionHighlightLink: '#2f81b7',
|
||||
codeTheme: 'github',
|
||||
image: mattermostThemeImage
|
||||
},
|
||||
mattermostDark: {
|
||||
type: 'Mattermost Dark',
|
||||
sidebarBg: '#1B2C3E',
|
||||
sidebarText: '#fff',
|
||||
sidebarUnreadText: '#fff',
|
||||
sidebarTextHoverBg: '#4A5664',
|
||||
sidebarTextActiveBorder: '#39769C',
|
||||
sidebarTextActiveColor: '#FFFFFF',
|
||||
sidebarHeaderBg: '#1B2C3E',
|
||||
sidebarHeaderTextColor: '#FFFFFF',
|
||||
onlineIndicator: '#55C5B2',
|
||||
awayIndicator: '#A9A14C',
|
||||
mentionBj: '#B74A4A',
|
||||
mentionColor: '#FFFFFF',
|
||||
centerChannelBg: '#2F3E4E',
|
||||
centerChannelColor: '#DDDDDD',
|
||||
newMessageSeparator: '#5de5da',
|
||||
linkColor: '#A4FFEB',
|
||||
buttonBg: '#4CBBA4',
|
||||
buttonColor: '#FFFFFF',
|
||||
mentionHighlightBg: '#984063',
|
||||
mentionHighlightLink: '#A4FFEB',
|
||||
codeTheme: 'solarized-dark',
|
||||
image: mattermostDarkThemeImage
|
||||
},
|
||||
windows10: {
|
||||
type: 'Windows Dark',
|
||||
sidebarBg: '#171717',
|
||||
sidebarText: '#fff',
|
||||
sidebarUnreadText: '#fff',
|
||||
sidebarTextHoverBg: '#302e30',
|
||||
sidebarTextActiveBorder: '#196CAF',
|
||||
sidebarTextActiveColor: '#FFFFFF',
|
||||
sidebarHeaderBg: '#1f1f1f',
|
||||
sidebarHeaderTextColor: '#FFFFFF',
|
||||
onlineIndicator: '#0177e7',
|
||||
awayIndicator: '#A9A14C',
|
||||
mentionBj: '#0177e7',
|
||||
mentionColor: '#FFFFFF',
|
||||
centerChannelBg: '#1F1F1F',
|
||||
centerChannelColor: '#DDDDDD',
|
||||
newMessageSeparator: '#CC992D',
|
||||
linkColor: '#0D93FF',
|
||||
buttonBg: '#0177e7',
|
||||
buttonColor: '#FFFFFF',
|
||||
mentionHighlightBg: '#784098',
|
||||
mentionHighlightLink: '#A4FFEB',
|
||||
codeTheme: 'monokai',
|
||||
image: windows10ThemeImage
|
||||
}
|
||||
},
|
||||
THEME_ELEMENTS: [
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarBg',
|
||||
uiName: 'Sidebar BG'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarText',
|
||||
uiName: 'Sidebar Text'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarHeaderBg',
|
||||
uiName: 'Sidebar Header BG'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarHeaderTextColor',
|
||||
uiName: 'Sidebar Header Text'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarUnreadText',
|
||||
uiName: 'Sidebar Unread Text'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarTextHoverBg',
|
||||
uiName: 'Sidebar Text Hover BG'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarTextActiveBorder',
|
||||
uiName: 'Sidebar Text Active Border'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'sidebarTextActiveColor',
|
||||
uiName: 'Sidebar Text Active Color'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'onlineIndicator',
|
||||
uiName: 'Online Indicator'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'awayIndicator',
|
||||
uiName: 'Away Indicator'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'mentionBj',
|
||||
uiName: 'Mention Jewel BG'
|
||||
},
|
||||
{
|
||||
group: 'sidebarElements',
|
||||
id: 'mentionColor',
|
||||
uiName: 'Mention Jewel Text'
|
||||
},
|
||||
{
|
||||
group: 'centerChannelElements',
|
||||
id: 'centerChannelBg',
|
||||
uiName: 'Center Channel BG'
|
||||
},
|
||||
{
|
||||
group: 'centerChannelElements',
|
||||
id: 'centerChannelColor',
|
||||
uiName: 'Center Channel Text'
|
||||
},
|
||||
{
|
||||
group: 'centerChannelElements',
|
||||
id: 'newMessageSeparator',
|
||||
uiName: 'New Message Separator'
|
||||
},
|
||||
{
|
||||
group: 'centerChannelElements',
|
||||
id: 'mentionHighlightBg',
|
||||
uiName: 'Mention Highlight BG'
|
||||
},
|
||||
{
|
||||
group: 'centerChannelElements',
|
||||
id: 'mentionHighlightLink',
|
||||
uiName: 'Mention Highlight Link'
|
||||
},
|
||||
{
|
||||
group: 'centerChannelElements',
|
||||
id: 'codeTheme',
|
||||
uiName: 'Code Theme',
|
||||
themes: [
|
||||
{
|
||||
id: 'solarized-dark',
|
||||
uiName: 'Solarized Dark',
|
||||
cssURL: solarizedDarkCSS,
|
||||
iconURL: solarizedDarkIcon
|
||||
},
|
||||
{
|
||||
id: 'solarized-light',
|
||||
uiName: 'Solarized Light',
|
||||
cssURL: solarizedLightCSS,
|
||||
iconURL: solarizedLightIcon
|
||||
},
|
||||
{
|
||||
id: 'github',
|
||||
uiName: 'GitHub',
|
||||
cssURL: githubCSS,
|
||||
iconURL: githubIcon
|
||||
},
|
||||
{
|
||||
id: 'monokai',
|
||||
uiName: 'Monokai',
|
||||
cssURL: monokaiCSS,
|
||||
iconURL: monokaiIcon
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: 'linkAndButtonElements',
|
||||
id: 'linkColor',
|
||||
uiName: 'Link Color'
|
||||
},
|
||||
{
|
||||
group: 'linkAndButtonElements',
|
||||
id: 'buttonBg',
|
||||
uiName: 'Button BG'
|
||||
},
|
||||
{
|
||||
group: 'linkAndButtonElements',
|
||||
id: 'buttonColor',
|
||||
uiName: 'Button Text'
|
||||
}
|
||||
],
|
||||
DEFAULT_CODE_THEME: 'github',
|
||||
FONTS: {
|
||||
'Droid Serif': 'font--droid_serif',
|
||||
'Roboto Slab': 'font--roboto_slab',
|
||||
Lora: 'font--lora',
|
||||
Arvo: 'font--arvo',
|
||||
'Open Sans': 'font--open_sans',
|
||||
Roboto: 'font--roboto',
|
||||
'PT Sans': 'font--pt_sans',
|
||||
Lato: 'font--lato',
|
||||
'Source Sans Pro': 'font--source_sans_pro',
|
||||
'Exo 2': 'font--exo_2',
|
||||
Ubuntu: 'font--ubuntu'
|
||||
},
|
||||
DEFAULT_FONT: 'Open Sans',
|
||||
Preferences: {
|
||||
CATEGORY_DIRECT_CHANNEL_SHOW: 'direct_channel_show',
|
||||
CATEGORY_DISPLAY_SETTINGS: 'display_settings',
|
||||
DISPLAY_PREFER_NICKNAME: 'nickname_full_name',
|
||||
DISPLAY_PREFER_FULL_NAME: 'full_name',
|
||||
CATEGORY_ADVANCED_SETTINGS: 'advanced_settings',
|
||||
TUTORIAL_STEP: 'tutorial_step'
|
||||
},
|
||||
TutorialSteps: {
|
||||
INTRO_SCREENS: 0,
|
||||
POST_POPOVER: 1,
|
||||
CHANNEL_POPOVER: 2,
|
||||
MENU_POPOVER: 3
|
||||
},
|
||||
KeyCodes: {
|
||||
UP: 38,
|
||||
DOWN: 40,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
BACKSPACE: 8,
|
||||
ENTER: 13,
|
||||
ESCAPE: 27,
|
||||
SPACE: 32,
|
||||
TAB: 9
|
||||
},
|
||||
HighlightedLanguages: {
|
||||
diff: 'Diff',
|
||||
apache: 'Apache',
|
||||
makefile: 'Makefile',
|
||||
http: 'HTTP',
|
||||
json: 'JSON',
|
||||
markdown: 'Markdown',
|
||||
javascript: 'JavaScript',
|
||||
css: 'CSS',
|
||||
nginx: 'nginx',
|
||||
objectivec: 'Objective-C',
|
||||
python: 'Python',
|
||||
xml: 'XML',
|
||||
perl: 'Perl',
|
||||
bash: 'Bash',
|
||||
php: 'PHP',
|
||||
coffeescript: 'CoffeeScript',
|
||||
cs: 'C#',
|
||||
cpp: 'C++',
|
||||
sql: 'SQL',
|
||||
go: 'Go',
|
||||
ruby: 'Ruby',
|
||||
java: 'Java',
|
||||
ini: 'ini'
|
||||
},
|
||||
PostsViewJumpTypes: {
|
||||
BOTTOM: 1,
|
||||
POST: 2,
|
||||
SIDEBAR_OPEN: 3
|
||||
},
|
||||
NotificationPrefs: {
|
||||
MENTION: 'mention'
|
||||
},
|
||||
FeatureTogglePrefix: 'feature_enabled_',
|
||||
PRE_RELEASE_FEATURES: {
|
||||
MARKDOWN_PREVIEW: {
|
||||
label: 'markdown_preview', // github issue: https://github.com/mattermost/platform/pull/1389
|
||||
description: 'Show markdown preview option in message input box'
|
||||
},
|
||||
EMBED_PREVIEW: {
|
||||
label: 'embed_preview',
|
||||
description: 'Show preview snippet of links below message'
|
||||
},
|
||||
EMBED_TOGGLE: {
|
||||
label: 'embed_toggle',
|
||||
description: 'Show toggle for all embed previews'
|
||||
}
|
||||
},
|
||||
OVERLAY_TIME_DELAY: 400,
|
||||
MIN_USERNAME_LENGTH: 3,
|
||||
MAX_USERNAME_LENGTH: 64,
|
||||
MIN_PASSWORD_LENGTH: 5,
|
||||
MAX_PASSWORD_LENGTH: 50,
|
||||
TIME_SINCE_UPDATE_INTERVAL: 30000,
|
||||
MIN_HASHTAG_LINK_LENGTH: 3
|
||||
};
|
||||
27
webapp/utils/delayed_action.jsx
Обычный файл
27
webapp/utils/delayed_action.jsx
Обычный файл
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
export default class DelayedAction {
|
||||
constructor(action) {
|
||||
this.action = action;
|
||||
|
||||
this.timer = -1;
|
||||
|
||||
// bind fire since it doesn't get passed the correct this value with setTimeout
|
||||
this.fire = this.fire.bind(this);
|
||||
}
|
||||
|
||||
fire() {
|
||||
this.action();
|
||||
|
||||
this.timer = -1;
|
||||
}
|
||||
|
||||
fireAfter(timeout) {
|
||||
if (this.timer >= 0) {
|
||||
window.clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
this.timer = window.setTimeout(this.fire, timeout);
|
||||
}
|
||||
}
|
||||
162
webapp/utils/emoticons.jsx
Обычный файл
162
webapp/utils/emoticons.jsx
Обычный файл
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
const emoticonPatterns = {
|
||||
slightly_smiling_face: /(^|\s)(:-?\))(?=$|\s)/g, // :)
|
||||
wink: /(^|\s)(;-?\))(?=$|\s)/g, // ;)
|
||||
open_mouth: /(^|\s)(:o)(?=$|\s)/gi, // :o
|
||||
scream: /(^|\s)(:-o)(?=$|\s)/gi, // :-o
|
||||
smirk: /(^|\s)(:-?])(?=$|\s)/g, // :]
|
||||
smile: /(^|\s)(:-?d)(?=$|\s)/gi, // :D
|
||||
stuck_out_tongue_closed_eyes: /(^|\s)(x-d)(?=$|\s)/gi, // x-d
|
||||
stuck_out_tongue: /(^|\s)(:-?p)(?=$|\s)/gi, // :p
|
||||
rage: /(^|\s)(:-?[\[@])(?=$|\s)/g, // :@
|
||||
slightly_frowning_face: /(^|\s)(:-?\()(?=$|\s)/g, // :(
|
||||
cry: /(^|\s)(:['’]-?\(|:'\(|:'\()(?=$|\s)/g, // :`(
|
||||
confused: /(^|\s)(:-?\/)(?=$|\s)/g, // :/
|
||||
confounded: /(^|\s)(:-?s)(?=$|\s)/gi, // :s
|
||||
neutral_face: /(^|\s)(:-?\|)(?=$|\s)/g, // :|
|
||||
flushed: /(^|\s)(:-?\$)(?=$|\s)/g, // :$
|
||||
mask: /(^|\s)(:-x)(?=$|\s)/gi, // :-x
|
||||
heart: /(^|\s)(<3|<3)(?=$|\s)/g, // <3
|
||||
broken_heart: /(^|\s)(<\/3|</3)(?=$|\s)/g, // </3
|
||||
thumbsup: /(^|\s)(:\+1:)(?=$|\s)/g, // :+1:
|
||||
thumbsdown: /(^|\s)(:\-1:)(?=$|\s)/g // :-1:
|
||||
};
|
||||
|
||||
function initializeEmoticonMap() {
|
||||
const emoticonNames =
|
||||
('+1,-1,100,1234,8ball,a,ab,abc,abcd,accept,aerial_tramway,airplane,alarm_clock,alien,ambulance,anchor,angel,' +
|
||||
'anger,angry,anguished,ant,apple,aquarius,aries,arrow_backward,arrow_double_down,arrow_double_up,arrow_down,' +
|
||||
'arrow_down_small,arrow_forward,arrow_heading_down,arrow_heading_up,arrow_left,arrow_lower_left,' +
|
||||
'arrow_lower_right,arrow_right,arrow_right_hook,arrow_up,arrow_up_down,arrow_up_small,arrow_upper_left,' +
|
||||
'arrow_upper_right,arrows_clockwise,arrows_counterclockwise,art,articulated_lorry,astonished,atm,b,baby,' +
|
||||
'baby_bottle,baby_chick,baby_symbol,back,baggage_claim,balloon,ballot_box_with_check,bamboo,banana,bangbang,' +
|
||||
'bank,bar_chart,barber,baseball,basketball,bath,bathtub,battery,bear,bee,beer,beers,beetle,beginner,bell,bento,' +
|
||||
'bicyclist,bike,bikini,bird,birthday,black_circle,black_joker,black_medium_small_square,black_medium_square,' +
|
||||
'black_large_square,black_nib,black_small_square,black_square,black_square_button,blossom,blowfish,blue_book,' +
|
||||
'blue_car,blue_heart,blush,boar,boat,bomb,book,bookmark,bookmark_tabs,books,boom,boot,bouquet,bow,bowling,bowtie,' +
|
||||
'boy,bread,bride_with_veil,bridge_at_night,briefcase,broken_heart,bug,bulb,bullettrain_front,bullettrain_side,bus,' +
|
||||
'busstop,bust_in_silhouette,busts_in_silhouette,cactus,cake,calendar,calling,camel,camera,cancer,candy,capital_abcd,' +
|
||||
'capricorn,car,card_index,carousel_horse,cat,cat2,cd,chart,chart_with_downwards_trend,chart_with_upwards_trend,' +
|
||||
'checkered_flag,cherries,cherry_blossom,chestnut,chicken,children_crossing,chocolate_bar,christmas_tree,church,' +
|
||||
'cinema,circus_tent,city_sunrise,city_sunset,cl,clap,clapper,clipboard,clock1,clock10,clock1030,clock11,' +
|
||||
'clock1130,clock12,clock1230,clock130,clock2,clock230,clock3,clock330,clock4,clock430,clock5,clock530,clock6,' +
|
||||
'clock630,clock7,clock730,clock8,clock830,clock9,clock930,closed_book,closed_lock_with_key,closed_umbrella,cloud,' +
|
||||
'clubs,cn,cocktail,coffee,cold_sweat,collision,computer,confetti_ball,confounded,confused,congratulations,' +
|
||||
'construction,construction_worker,convenience_store,cookie,cool,cop,copyright,corn,couple,couple_with_heart,' +
|
||||
'couplekiss,cow,cow2,credit_card,crescent_moon,crocodile,crossed_flags,crown,cry,crying_cat_face,crystal_ball,' +
|
||||
'cupid,curly_loop,currency_exchange,curry,custard,customs,cyclone,dancer,dancers,dango,dart,dash,date,de,' +
|
||||
'deciduous_tree,department_store,diamond_shape_with_a_dot_inside,diamonds,disappointed,disappointed_relieved,' +
|
||||
'dizzy,dizzy_face,do_not_litter,dog,dog2,dollar,dolls,dolphin,donut,door,doughnut,dragon,dragon_face,dress,' +
|
||||
'dromedary_camel,droplet,dvd,e-mail,ear,ear_of_rice,earth_africa,earth_americas,earth_asia,egg,eggplant,eight,' +
|
||||
'eight_pointed_black_star,eight_spoked_asterisk,electric_plug,elephant,email,end,envelope,es,euro,' +
|
||||
'european_castle,european_post_office,evergreen_tree,exclamation,expressionless,eyeglasses,eyes,facepunch,' +
|
||||
'factory,fallen_leaf,family,fast_forward,fax,fearful,feelsgood,feet,ferris_wheel,file_folder,finnadie,fire,' +
|
||||
'fire_engine,fireworks,first_quarter_moon,first_quarter_moon_with_face,fish,fish_cake,fishing_pole_and_fish,fist,' +
|
||||
'five,flags,flashlight,floppy_disk,flower_playing_cards,flushed,foggy,football,fork_and_knife,fountain,four,' +
|
||||
'four_leaf_clover,fr,free,fried_shrimp,fries,frog,frowning,fu,fuelpump,full_moon,full_moon_with_face,game_die,gb,' +
|
||||
'gem,gemini,ghost,gift,gift_heart,girl,globe_with_meridians,goat,goberserk,godmode,golf,grapes,green_apple,' +
|
||||
'green_book,green_heart,grey_exclamation,grey_question,grimacing,grin,grinning,guardsman,guitar,gun,haircut,' +
|
||||
'hamburger,hammer,hamster,hand,handbag,hankey,hash,hatched_chick,hatching_chick,headphones,hear_no_evil,heart,' +
|
||||
'heart_decoration,heart_eyes,heart_eyes_cat,heartbeat,heartpulse,hearts,heavy_check_mark,heavy_division_sign,' +
|
||||
'heavy_dollar_sign,heavy_exclamation_mark,heavy_minus_sign,heavy_multiplication_x,heavy_plus_sign,helicopter,' +
|
||||
'herb,hibiscus,high_brightness,high_heel,hocho,honey_pot,honeybee,horse,horse_racing,hospital,hotel,hotsprings,' +
|
||||
'hourglass,hourglass_flowing_sand,house,house_with_garden,hurtrealbad,hushed,ice_cream,icecream,id,' +
|
||||
'ideograph_advantage,imp,inbox_tray,incoming_envelope,information_desk_person,information_source,innocent,' +
|
||||
'interrobang,iphone,it,izakaya_lantern,jack_o_lantern,japan,japanese_castle,japanese_goblin,japanese_ogre,jeans,' +
|
||||
'joy,joy_cat,jp,key,keycap_ten,kimono,kiss,kissing,kissing_cat,kissing_closed_eyes,kissing_face,kissing_heart,' +
|
||||
'kissing_smiling_eyes,koala,koko,kr,large_blue_circle,large_blue_diamond,large_orange_diamond,last_quarter_moon,' +
|
||||
'last_quarter_moon_with_face,laughing,leaves,ledger,left_luggage,left_right_arrow,leftwards_arrow_with_hook,' +
|
||||
'lemon,leo,leopard,libra,light_rail,link,lips,lipstick,lock,lock_with_ink_pen,lollipop,loop,loudspeaker,' +
|
||||
'love_hotel,love_letter,low_brightness,m,mag,mag_right,mahjong,mailbox,mailbox_closed,mailbox_with_mail,' +
|
||||
'mailbox_with_no_mail,man,man_with_gua_pi_mao,man_with_turban,mans_shoe,maple_leaf,mask,massage,meat_on_bone,' +
|
||||
'mega,melon,memo,mens,metal,metro,microphone,microscope,milky_way,minibus,minidisc,mobile_phone_off,' +
|
||||
'money_with_wings,moneybag,monkey,monkey_face,monorail,mortar_board,mount_fuji,mountain_bicyclist,' +
|
||||
'mountain_cableway,mountain_railway,mouse,mouse2,movie_camera,moyai,muscle,mushroom,musical_keyboard,' +
|
||||
'musical_note,musical_score,mute,nail_care,name_badge,neckbeard,necktie,negative_squared_cross_mark,' +
|
||||
'neutral_face,new,new_moon,new_moon_with_face,newspaper,ng,nine,no_bell,no_bicycles,no_entry,no_entry_sign,' +
|
||||
'no_good,no_mobile_phones,no_mouth,no_pedestrians,no_smoking,non-potable_water,nose,notebook,' +
|
||||
'notebook_with_decorative_cover,notes,nut_and_bolt,o,o2,ocean,octocat,octopus,oden,office,ok,ok_hand,' +
|
||||
'ok_woman,older_man,older_woman,on,oncoming_automobile,oncoming_bus,oncoming_police_car,oncoming_taxi,one,' +
|
||||
'open_file_folder,open_hands,open_mouth,ophiuchus,orange_book,outbox_tray,ox,package,page_facing_up,' +
|
||||
'page_with_curl,pager,palm_tree,panda_face,paperclip,parking,part_alternation_mark,partly_sunny,' +
|
||||
'passport_control,paw_prints,peach,pear,pencil,pencil2,penguin,pensive,performing_arts,persevere,' +
|
||||
'person_frowning,person_with_blond_hair,person_with_pouting_face,phone,pig,pig2,pig_nose,pill,pineapple,pisces,' +
|
||||
'pizza,plus1,point_down,point_left,point_right,point_up,point_up_2,police_car,poodle,poop,post_office,' +
|
||||
'postal_horn,postbox,potable_water,pouch,poultry_leg,pound,pouting_cat,pray,princess,punch,purple_heart,purse,' +
|
||||
'pushpin,put_litter_in_its_place,question,rabbit,rabbit2,racehorse,radio,radio_button,rage,rage1,rage2,rage3,' +
|
||||
'rage4,railway_car,rainbow,raised_hand,raised_hands,raising_hand,ram,ramen,rat,recycle,red_car,red_circle,' +
|
||||
'registered,relaxed,relieved,repeat,repeat_one,restroom,revolving_hearts,rewind,ribbon,rice,rice_ball,' +
|
||||
'rice_cracker,rice_scene,ring,rocket,roller_coaster,rooster,rose,rotating_light,round_pushpin,rowboat,ru,' +
|
||||
'rugby_football,runner,running,running_shirt_with_sash,sa,sagittarius,sailboat,sake,sandal,santa,satellite,' +
|
||||
'satisfied,saxophone,school,school_satchel,scissors,scorpius,scream,scream_cat,scroll,seat,secret,see_no_evil,' +
|
||||
'seedling,seven,shaved_ice,sheep,shell,ship,shipit,shirt,shit,shoe,shower,signal_strength,six,six_pointed_star,' +
|
||||
'ski,skull,sleeping,sleepy,slightly_smiling_face,slightly_frowning_face,slot_machine,small_blue_diamond,' +
|
||||
'small_orange_diamond,small_red_triangle,small_red_triangle_down,smile,smile_cat,smiley,smiley_cat,smiling_imp,' +
|
||||
'smirk,smirk_cat,smoking,snail,snake,snowboarder,snowflake,snowman,sob,soccer,soon,sos,sound,space_invader,spades,' +
|
||||
'spaghetti,sparkle,sparkler,sparkles,sparkling_heart,speak_no_evil,speaker,speech_balloon,speedboat,squirrel,star,' +
|
||||
'star2,stars,station,statue_of_liberty,steam_locomotive,stew,straight_ruler,strawberry,stuck_out_tongue,' +
|
||||
'stuck_out_tongue_closed_eyes,stuck_out_tongue_winking_eye,sun_with_face,sunflower,sunglasses,sunny,sunrise,' +
|
||||
'sunrise_over_mountains,surfer,sushi,suspect,suspension_railway,sweat,sweat_drops,sweat_smile,sweet_potato,swimmer,' +
|
||||
'symbols,syringe,tada,tanabata_tree,tangerine,taurus,taxi,tea,telephone,telephone_receiver,telescope,tennis,tent,' +
|
||||
'thought_balloon,three,thumbsdown,thumbsup,ticket,tiger,tiger2,tired_face,tm,toilet,tokyo_tower,tomato,tongue,top,' +
|
||||
'tophat,tractor,traffic_light,train,train2,tram,triangular_flag_on_post,triangular_ruler,trident,triumph,trolleybus,' +
|
||||
'trollface,trophy,tropical_drink,tropical_fish,truck,trumpet,tshirt,tulip,turtle,tv,twisted_rightwards_arrows,' +
|
||||
'two,two_hearts,two_men_holding_hands,two_women_holding_hands,u5272,u5408,u55b6,u6307,u6708,u6709,u6e80,u7121,' +
|
||||
'u7533,u7981,u7a7a,uk,umbrella,unamused,underage,unlock,up,us,v,vertical_traffic_light,vhs,vibration_mode,' +
|
||||
'video_camera,video_game,violin,virgo,volcano,vs,walking,waning_crescent_moon,waning_gibbous_moon,warning,watch,' +
|
||||
'water_buffalo,watermelon,wave,wavy_dash,waxing_crescent_moon,waxing_gibbous_moon,wc,weary,wedding,whale,whale2,' +
|
||||
'wheelchair,white_check_mark,white_circle,white_flower,white_large_square,white_medium_small_square,' +
|
||||
'white_medium_square,white_small_square,white_square_button,wind_chime,wine_glass,wink,wolf,woman,' +
|
||||
'womans_clothes,womans_hat,womens,worried,wrench,x,yellow_heart,yen,yum,zap,zero,zzz').split(',');
|
||||
|
||||
// use a map to help make lookups faster instead of having to use indexOf on an array
|
||||
const out = new Map();
|
||||
|
||||
for (let i = 0; i < emoticonNames.length; i++) {
|
||||
out.set(emoticonNames[i], true);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
export const emoticonMap = initializeEmoticonMap();
|
||||
|
||||
export function handleEmoticons(text, tokens) {
|
||||
let output = text;
|
||||
|
||||
function replaceEmoticonWithToken(fullMatch, prefix, matchText, name) {
|
||||
if (emoticonMap.has(name)) {
|
||||
const index = tokens.size;
|
||||
const alias = `MM_EMOTICON${index}`;
|
||||
|
||||
tokens.set(alias, {
|
||||
value: `<img align="absmiddle" alt="${matchText}" class="emoticon" src="${getImagePathForEmoticon(name)}" title="${matchText}" />`,
|
||||
originalText: fullMatch
|
||||
});
|
||||
|
||||
return prefix + alias;
|
||||
}
|
||||
|
||||
return fullMatch;
|
||||
}
|
||||
|
||||
output = output.replace(/(^|\s)(:([a-zA-Z0-9_-]+):)(?=$|\s)/g, (fullMatch, prefix, matchText, name) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
|
||||
|
||||
$.each(emoticonPatterns, (name, pattern) => {
|
||||
// this might look a bit funny, but since the name isn't contained in the actual match
|
||||
// like with the named emoticons, we need to add it in manually
|
||||
output = output.replace(pattern, (fullMatch, prefix, matchText) => replaceEmoticonWithToken(fullMatch, prefix, matchText, name));
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function getImagePathForEmoticon(name) {
|
||||
if (name) {
|
||||
return `/static/emoji/${name}.png`;
|
||||
}
|
||||
return '/static/emoji';
|
||||
}
|
||||
577
webapp/utils/markdown.jsx
Обычный файл
577
webapp/utils/markdown.jsx
Обычный файл
@@ -0,0 +1,577 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import highlightJs from 'highlight.js/lib/highlight.js';
|
||||
import highlightJsDiff from 'highlight.js/lib/languages/diff.js';
|
||||
import highlightJsApache from 'highlight.js/lib/languages/apache.js';
|
||||
import highlightJsMakefile from 'highlight.js/lib/languages/makefile.js';
|
||||
import highlightJsHttp from 'highlight.js/lib/languages/http.js';
|
||||
import highlightJsJson from 'highlight.js/lib/languages/json.js';
|
||||
import highlightJsMarkdown from 'highlight.js/lib/languages/markdown.js';
|
||||
import highlightJsJavascript from 'highlight.js/lib/languages/javascript.js';
|
||||
import highlightJsCss from 'highlight.js/lib/languages/css.js';
|
||||
import highlightJsNginx from 'highlight.js/lib/languages/nginx.js';
|
||||
import highlightJsObjectivec from 'highlight.js/lib/languages/objectivec.js';
|
||||
import highlightJsPython from 'highlight.js/lib/languages/python.js';
|
||||
import highlightJsXml from 'highlight.js/lib/languages/xml.js';
|
||||
import highlightJsPerl from 'highlight.js/lib/languages/perl.js';
|
||||
import highlightJsBash from 'highlight.js/lib/languages/bash.js';
|
||||
import highlightJsPhp from 'highlight.js/lib/languages/php.js';
|
||||
import highlightJsCoffeescript from 'highlight.js/lib/languages/coffeescript.js';
|
||||
import highlightJsCs from 'highlight.js/lib/languages/cs.js';
|
||||
import highlightJsCpp from 'highlight.js/lib/languages/cpp.js';
|
||||
import highlightJsSql from 'highlight.js/lib/languages/sql.js';
|
||||
import highlightJsGo from 'highlight.js/lib/languages/go.js';
|
||||
import highlightJsRuby from 'highlight.js/lib/languages/ruby.js';
|
||||
import highlightJsJava from 'highlight.js/lib/languages/java.js';
|
||||
import highlightJsIni from 'highlight.js/lib/languages/ini.js';
|
||||
|
||||
highlightJs.registerLanguage('diff', highlightJsDiff);
|
||||
highlightJs.registerLanguage('apache', highlightJsApache);
|
||||
highlightJs.registerLanguage('makefile', highlightJsMakefile);
|
||||
highlightJs.registerLanguage('http', highlightJsHttp);
|
||||
highlightJs.registerLanguage('json', highlightJsJson);
|
||||
highlightJs.registerLanguage('markdown', highlightJsMarkdown);
|
||||
highlightJs.registerLanguage('javascript', highlightJsJavascript);
|
||||
highlightJs.registerLanguage('css', highlightJsCss);
|
||||
highlightJs.registerLanguage('nginx', highlightJsNginx);
|
||||
highlightJs.registerLanguage('objectivec', highlightJsObjectivec);
|
||||
highlightJs.registerLanguage('python', highlightJsPython);
|
||||
highlightJs.registerLanguage('xml', highlightJsXml);
|
||||
highlightJs.registerLanguage('perl', highlightJsPerl);
|
||||
highlightJs.registerLanguage('bash', highlightJsBash);
|
||||
highlightJs.registerLanguage('php', highlightJsPhp);
|
||||
highlightJs.registerLanguage('coffeescript', highlightJsCoffeescript);
|
||||
highlightJs.registerLanguage('cs', highlightJsCs);
|
||||
highlightJs.registerLanguage('cpp', highlightJsCpp);
|
||||
highlightJs.registerLanguage('sql', highlightJsSql);
|
||||
highlightJs.registerLanguage('go', highlightJsGo);
|
||||
highlightJs.registerLanguage('ruby', highlightJsRuby);
|
||||
highlightJs.registerLanguage('java', highlightJsJava);
|
||||
highlightJs.registerLanguage('ini', highlightJsIni);
|
||||
|
||||
import * as TextFormatting from './text_formatting.jsx';
|
||||
import * as Utils from './utils.jsx';
|
||||
|
||||
import marked from 'marked';
|
||||
import katex from 'katex';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const HighlightedLanguages = Constants.HighlightedLanguages;
|
||||
|
||||
function markdownImageLoaded(image) {
|
||||
image.style.height = 'auto';
|
||||
}
|
||||
window.markdownImageLoaded = markdownImageLoaded;
|
||||
|
||||
class MattermostInlineLexer extends marked.InlineLexer {
|
||||
constructor(links, options) {
|
||||
super(links, options);
|
||||
|
||||
this.rules = Object.assign({}, this.rules);
|
||||
|
||||
// modified version of the regex that allows for links starting with www and those surrounded by parentheses
|
||||
// the original is /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/| {2,}\n|$)/
|
||||
this.rules.text = /^[\s\S]+?(?=[\\<!\[_*`~]|https?:\/\/|www\.|\(| {2,}\n|$)/;
|
||||
|
||||
// modified version of the regex that allows links starting with www and those surrounded by parentheses
|
||||
// the original is /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/
|
||||
this.rules.url = /^(\(?(?:https?:\/\/|www\.)[^\s<.][^\s<]*[^<.,:;"'\]\s])/;
|
||||
|
||||
// modified version of the regex that allows <links> starting with www.
|
||||
// the original is /^<([^ >]+(@|:\/)[^ >]+)>/
|
||||
this.rules.autolink = /^<((?:[^ >]+(@|:\/)|www\.)[^ >]+)>/;
|
||||
}
|
||||
}
|
||||
|
||||
class MattermostParser extends marked.Parser {
|
||||
parse(src) {
|
||||
this.inline = new MattermostInlineLexer(src.links, this.options, this.renderer);
|
||||
this.tokens = src.reverse();
|
||||
|
||||
var out = '';
|
||||
while (this.next()) {
|
||||
out += this.tok();
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
class MattermostMarkdownRenderer extends marked.Renderer {
|
||||
constructor(options, formattingOptions = {}) {
|
||||
super(options);
|
||||
|
||||
this.heading = this.heading.bind(this);
|
||||
this.paragraph = this.paragraph.bind(this);
|
||||
this.text = this.text.bind(this);
|
||||
|
||||
this.formattingOptions = formattingOptions;
|
||||
}
|
||||
|
||||
code(code, language, escaped) {
|
||||
let usedLanguage = language || '';
|
||||
usedLanguage = usedLanguage.toLowerCase();
|
||||
|
||||
// treat html as xml to prevent injection attacks
|
||||
if (usedLanguage === 'html') {
|
||||
usedLanguage = 'xml';
|
||||
}
|
||||
|
||||
if (HighlightedLanguages[usedLanguage]) {
|
||||
const parsed = highlightJs.highlight(usedLanguage, code);
|
||||
|
||||
return (
|
||||
'<div class="post-body--code">' +
|
||||
'<span class="post-body--code__language">' +
|
||||
HighlightedLanguages[usedLanguage] +
|
||||
'</span>' +
|
||||
'<pre>' +
|
||||
'<code class="hljs">' +
|
||||
parsed.value +
|
||||
'</code>' +
|
||||
'</pre>' +
|
||||
'</div>'
|
||||
);
|
||||
} else if (usedLanguage === 'tex' || usedLanguage === 'latex') {
|
||||
try {
|
||||
const html = katex.renderToString(code, {throwOnError: false, displayMode: true});
|
||||
|
||||
return '<div class="post-body--code tex">' + html + '</div>';
|
||||
} catch (e) {
|
||||
// fall through if latex parsing fails and handle below
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
'<pre>' +
|
||||
'<code class="hljs">' +
|
||||
(escaped ? code : TextFormatting.sanitizeHtml(code)) + '\n' +
|
||||
'</code>' +
|
||||
'</pre>'
|
||||
);
|
||||
}
|
||||
|
||||
codespan(text) {
|
||||
return '<span class="codespan__pre-wrap">' + super.codespan(text) + '</span>';
|
||||
}
|
||||
|
||||
br() {
|
||||
if (this.formattingOptions.singleline) {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
return super.br();
|
||||
}
|
||||
|
||||
image(href, title, text) {
|
||||
let out = '<img src="' + href + '" alt="' + text + '"';
|
||||
if (title) {
|
||||
out += ' title="' + title + '"';
|
||||
}
|
||||
out += ' onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"';
|
||||
out += this.options.xhtml ? '/>' : '>';
|
||||
return out;
|
||||
}
|
||||
|
||||
heading(text, level, raw) {
|
||||
const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
|
||||
return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
|
||||
}
|
||||
|
||||
link(href, title, text) {
|
||||
let outHref = href;
|
||||
let outText = text;
|
||||
let prefix = '';
|
||||
let suffix = '';
|
||||
|
||||
// some links like https://en.wikipedia.org/wiki/Rendering_(computer_graphics) contain brackets
|
||||
// and we try our best to differentiate those from ones just wrapped in brackets when autolinking
|
||||
if (outHref.startsWith('(') && outHref.endsWith(')') && text === outHref) {
|
||||
prefix = '(';
|
||||
suffix = ')';
|
||||
outText = text.substring(1, text.length - 1);
|
||||
outHref = outHref.substring(1, outHref.length - 1);
|
||||
}
|
||||
|
||||
try {
|
||||
const unescaped = decodeURIComponent(unescape(href)).replace(/[^\w:]/g, '').toLowerCase();
|
||||
|
||||
if (unescaped.indexOf('javascript:') === 0 || unescaped.indexOf('vbscript:') === 0) { // eslint-disable-line no-script-url
|
||||
return '';
|
||||
}
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!(/[a-z+.-]+:/i).test(outHref)) {
|
||||
outHref = `http://${outHref}`;
|
||||
}
|
||||
|
||||
let output = '<a class="theme markdown__link" href="' + outHref + '"';
|
||||
if (title) {
|
||||
output += ' title="' + title + '"';
|
||||
}
|
||||
|
||||
if (outHref.lastIndexOf(Utils.getTeamURLFromAddressBar(), 0) === 0) {
|
||||
output += '>';
|
||||
} else {
|
||||
output += ' target="_blank">';
|
||||
}
|
||||
|
||||
output += outText + '</a>';
|
||||
|
||||
return prefix + output + suffix;
|
||||
}
|
||||
|
||||
paragraph(text) {
|
||||
if (this.formattingOptions.singleline) {
|
||||
return `<p class="markdown__paragraph-inline">${text}</p>`;
|
||||
}
|
||||
|
||||
return super.paragraph(text);
|
||||
}
|
||||
|
||||
table(header, body) {
|
||||
return `<div class="table-responsive"><table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
listitem(text) {
|
||||
const taskListReg = /^\[([ |xX])\] /;
|
||||
const isTaskList = taskListReg.exec(text);
|
||||
|
||||
if (isTaskList) {
|
||||
return `<li class="list-item--task-list">${'<input type="checkbox" disabled="disabled" ' + (isTaskList[1] === ' ' ? '' : 'checked="checked" ') + '/> '}${text.replace(taskListReg, '')}</li>`;
|
||||
}
|
||||
return `<li>${text}</li>`;
|
||||
}
|
||||
|
||||
text(txt) {
|
||||
return TextFormatting.doFormatText(txt, this.formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
class MattermostLexer extends marked.Lexer {
|
||||
token(originalSrc, top, bq) {
|
||||
let src = originalSrc.replace(/^ +$/gm, '');
|
||||
|
||||
while (src) {
|
||||
// newline
|
||||
let cap = this.rules.newline.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
if (cap[0].length > 1) {
|
||||
this.tokens.push({
|
||||
type: 'space'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// code
|
||||
cap = this.rules.code.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
cap = cap[0].replace(/^ {4}/gm, '');
|
||||
this.tokens.push({
|
||||
type: 'code',
|
||||
text: this.options.pedantic ? cap : cap.replace(/\n+$/, '')
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// fences (gfm)
|
||||
cap = this.rules.fences.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.push({
|
||||
type: 'code',
|
||||
lang: cap[2],
|
||||
text: cap[3] || ''
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// heading
|
||||
cap = this.rules.heading.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.push({
|
||||
type: 'heading',
|
||||
depth: cap[1].length,
|
||||
text: cap[2]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// table no leading pipe (gfm)
|
||||
cap = this.rules.nptable.exec(src);
|
||||
if (top && cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
|
||||
const item = {
|
||||
type: 'table',
|
||||
header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
|
||||
align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
|
||||
cells: cap[3].replace(/\n$/, '').split('\n')
|
||||
};
|
||||
|
||||
for (let i = 0; i < item.align.length; i++) {
|
||||
if (/^ *-+: *$/.test(item.align[i])) {
|
||||
item.align[i] = 'right';
|
||||
} else if (/^ *:-+: *$/.test(item.align[i])) {
|
||||
item.align[i] = 'center';
|
||||
} else if (/^ *:-+ *$/.test(item.align[i])) {
|
||||
item.align[i] = 'left';
|
||||
} else {
|
||||
item.align[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < item.cells.length; i++) {
|
||||
item.cells[i] = item.cells[i].split(/ *\| */);
|
||||
}
|
||||
|
||||
this.tokens.push(item);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// lheading
|
||||
cap = this.rules.lheading.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.push({
|
||||
type: 'heading',
|
||||
depth: cap[2] === '=' ? 1 : 2,
|
||||
text: cap[1]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// hr
|
||||
cap = this.rules.hr.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.push({
|
||||
type: 'hr'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// blockquote
|
||||
cap = this.rules.blockquote.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
|
||||
this.tokens.push({
|
||||
type: 'blockquote_start'
|
||||
});
|
||||
|
||||
cap = cap[0].replace(/^ *> ?/gm, '');
|
||||
|
||||
// Pass `top` to keep the current
|
||||
// "toplevel" state. This is exactly
|
||||
// how markdown.pl works.
|
||||
this.token(cap, top, true);
|
||||
|
||||
this.tokens.push({
|
||||
type: 'blockquote_end'
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// list
|
||||
cap = this.rules.list.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
const bull = cap[2];
|
||||
|
||||
this.tokens.push({
|
||||
type: 'list_start',
|
||||
ordered: bull.length > 1
|
||||
});
|
||||
|
||||
// Get each top-level item.
|
||||
cap = cap[0].match(this.rules.item);
|
||||
|
||||
let next = false;
|
||||
const l = cap.length;
|
||||
let i = 0;
|
||||
|
||||
for (; i < l; i++) {
|
||||
let item = cap[i];
|
||||
|
||||
// Remove the list item's bullet
|
||||
// so it is seen as the next token.
|
||||
let space = item.length;
|
||||
item = item.replace(/^ *([*+-]|\d+\.) +/, '');
|
||||
|
||||
// Outdent whatever the
|
||||
// list item contains. Hacky.
|
||||
if (~item.indexOf('\n ')) {
|
||||
space -= item.length;
|
||||
item = this.options.pedantic ?
|
||||
item.replace(/^ {1,4}/gm, '') :
|
||||
item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '');
|
||||
}
|
||||
|
||||
// Determine whether the next list item belongs here.
|
||||
// Backpedal if it does not belong in this list.
|
||||
if (this.options.smartLists && i !== l - 1) {
|
||||
const b = this.rules.bullet.exec(cap[i + 1])[0];
|
||||
if (bull !== b && !(bull.length > 1 && b.length > 1)) {
|
||||
src = cap.slice(i + 1).join('\n') + src;
|
||||
i = l - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether item is loose or not.
|
||||
// Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
|
||||
// for discount behavior.
|
||||
let loose = next || (/\n\n(?!\s*$)/).test(item);
|
||||
if (i !== l - 1) {
|
||||
next = item.charAt(item.length - 1) === '\n';
|
||||
if (!loose) {
|
||||
loose = next;
|
||||
}
|
||||
}
|
||||
|
||||
this.tokens.push({
|
||||
type: loose ?
|
||||
'loose_item_start' :
|
||||
'list_item_start'
|
||||
});
|
||||
|
||||
// Recurse.
|
||||
this.token(item, false, bq);
|
||||
|
||||
this.tokens.push({
|
||||
type: 'list_item_end'
|
||||
});
|
||||
}
|
||||
|
||||
this.tokens.push({
|
||||
type: 'list_end'
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// html
|
||||
cap = this.rules.html.exec(src);
|
||||
if (cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.push({
|
||||
type: this.options.sanitize ? 'paragraph' : 'html',
|
||||
pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
|
||||
text: cap[0]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// def
|
||||
cap = this.rules.def.exec(src);
|
||||
if ((!bq && top) && cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.links[cap[1].toLowerCase()] = {
|
||||
href: cap[2],
|
||||
title: cap[3]
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// table (gfm)
|
||||
cap = this.rules.table.exec(src);
|
||||
if (top && cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
|
||||
const item = {
|
||||
type: 'table',
|
||||
header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
|
||||
align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
|
||||
cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
|
||||
};
|
||||
|
||||
for (let i = 0; i < item.align.length; i++) {
|
||||
if (/^ *-+: *$/.test(item.align[i])) {
|
||||
item.align[i] = 'right';
|
||||
} else if (/^ *:-+: *$/.test(item.align[i])) {
|
||||
item.align[i] = 'center';
|
||||
} else if (/^ *:-+ *$/.test(item.align[i])) {
|
||||
item.align[i] = 'left';
|
||||
} else {
|
||||
item.align[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < item.cells.length; i++) {
|
||||
item.cells[i] = item.cells[i].replace(/^ *\| *| *\| *$/g, '').split(/ *\| */);
|
||||
}
|
||||
|
||||
this.tokens.push(item);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// top-level paragraph
|
||||
cap = this.rules.paragraph.exec(src);
|
||||
if (top && cap) {
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.push({
|
||||
type: 'paragraph',
|
||||
text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// text
|
||||
cap = this.rules.text.exec(src);
|
||||
if (cap) {
|
||||
// Top-level should never reach here.
|
||||
src = src.substring(cap[0].length);
|
||||
this.tokens.push({
|
||||
type: 'text',
|
||||
text: cap[0]
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (src) {
|
||||
throw new Error('Infinite loop on byte: ' + src.charCodeAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
return this.tokens;
|
||||
}
|
||||
}
|
||||
|
||||
export function format(text, options) {
|
||||
const markdownOptions = {
|
||||
renderer: new MattermostMarkdownRenderer(null, options),
|
||||
sanitize: true,
|
||||
gfm: true,
|
||||
tables: true
|
||||
};
|
||||
|
||||
const tokens = new MattermostLexer(markdownOptions).lex(text);
|
||||
|
||||
return new MattermostParser(markdownOptions).parse(tokens);
|
||||
}
|
||||
|
||||
// Marked helper functions that should probably just be exported
|
||||
|
||||
function unescape(html) {
|
||||
return html.replace(/&([#\w]+);/g, (_, m) => {
|
||||
const n = m.toLowerCase();
|
||||
if (n === 'colon') {
|
||||
return ':';
|
||||
} else if (n.charAt(0) === '#') {
|
||||
return n.charAt(1) === 'x' ?
|
||||
String.fromCharCode(parseInt(n.substring(2), 16)) :
|
||||
String.fromCharCode(+n.substring(1));
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}
|
||||
402
webapp/utils/text_formatting.jsx
Обычный файл
402
webapp/utils/text_formatting.jsx
Обычный файл
@@ -0,0 +1,402 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Autolinker from 'autolinker';
|
||||
import Constants from './constants.jsx';
|
||||
import * as Emoticons from './emoticons.jsx';
|
||||
import * as Markdown from './markdown.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from './utils.jsx';
|
||||
|
||||
// Performs formatting of user posts including highlighting mentions and search terms and converting urls, hashtags, and
|
||||
// @mentions to links by taking a user's message and returning a string of formatted html. Also takes a number of options
|
||||
// as part of the second parameter:
|
||||
// - searchTerm - If specified, this word is highlighted in the resulting html. Defaults to nothing.
|
||||
// - mentionHighlight - Specifies whether or not to highlight mentions of the current user. Defaults to true.
|
||||
// - singleline - Specifies whether or not to remove newlines. Defaults to false.
|
||||
// - emoticons - Enables emoticon parsing. Defaults to true.
|
||||
// - markdown - Enables markdown parsing. Defaults to true.
|
||||
export function formatText(text, options = {}) {
|
||||
let output;
|
||||
|
||||
if (!('markdown' in options) || options.markdown) {
|
||||
// the markdown renderer will call doFormatText as necessary
|
||||
output = Markdown.format(text, options);
|
||||
} else {
|
||||
output = sanitizeHtml(text);
|
||||
output = doFormatText(output, options);
|
||||
}
|
||||
|
||||
// replace newlines with spaces if necessary
|
||||
if (options.singleline) {
|
||||
output = replaceNewlines(output);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Performs most of the actual formatting work for formatText. Not intended to be called normally.
|
||||
export function doFormatText(text, options) {
|
||||
let output = text;
|
||||
|
||||
const tokens = new Map();
|
||||
|
||||
// replace important words and phrases with tokens
|
||||
output = autolinkAtMentions(output, tokens);
|
||||
output = autolinkEmails(output, tokens);
|
||||
output = autolinkHashtags(output, tokens);
|
||||
|
||||
if (!('emoticons' in options) || options.emoticon) {
|
||||
output = Emoticons.handleEmoticons(output, tokens);
|
||||
}
|
||||
|
||||
if (options.searchTerm) {
|
||||
output = highlightSearchTerm(output, tokens, options.searchTerm);
|
||||
}
|
||||
|
||||
if (!('mentionHighlight' in options) || options.mentionHighlight) {
|
||||
output = highlightCurrentMentions(output, tokens);
|
||||
}
|
||||
|
||||
// reinsert tokens with formatted versions of the important words and phrases
|
||||
output = replaceTokens(output, tokens);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export function sanitizeHtml(text) {
|
||||
let output = text;
|
||||
|
||||
// normal string.replace only does a single occurrance so use a regex instead
|
||||
output = output.replace(/&/g, '&');
|
||||
output = output.replace(/</g, '<');
|
||||
output = output.replace(/>/g, '>');
|
||||
output = output.replace(/'/g, ''');
|
||||
output = output.replace(/"/g, '"');
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Convert emails into tokens
|
||||
function autolinkEmails(text, tokens) {
|
||||
function replaceEmailWithToken(autolinker, match) {
|
||||
const linkText = match.getMatchedText();
|
||||
let url = linkText;
|
||||
|
||||
if (match.getType() === 'email') {
|
||||
url = `mailto:${url}`;
|
||||
}
|
||||
|
||||
const index = tokens.size;
|
||||
const alias = `MM_EMAIL${index}`;
|
||||
|
||||
tokens.set(alias, {
|
||||
value: `<a class="theme" href="${url}">${linkText}</a>`,
|
||||
originalText: linkText
|
||||
});
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
// we can't just use a static autolinker because we need to set replaceFn
|
||||
const autolinker = new Autolinker({
|
||||
urls: false,
|
||||
email: true,
|
||||
phone: false,
|
||||
twitter: false,
|
||||
hashtag: false,
|
||||
replaceFn: replaceEmailWithToken
|
||||
});
|
||||
|
||||
return autolinker.link(text);
|
||||
}
|
||||
|
||||
function autolinkAtMentions(text, tokens) {
|
||||
// Return true if provided character is punctuation
|
||||
function isPunctuation(character) {
|
||||
const re = /[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]/g;
|
||||
return re.test(character);
|
||||
}
|
||||
|
||||
// Test if provided text needs to be highlighted, special mention or current user
|
||||
function mentionExists(u) {
|
||||
return (Constants.SPECIAL_MENTIONS.indexOf(u) !== -1 || UserStore.getProfileByUsername(u));
|
||||
}
|
||||
|
||||
function addToken(username, mention) {
|
||||
const index = tokens.size;
|
||||
const alias = `MM_ATMENTION${index}`;
|
||||
|
||||
tokens.set(alias, {
|
||||
value: `<a class='mention-link' href='#' data-mention='${username}'>${mention}</a>`,
|
||||
originalText: mention
|
||||
});
|
||||
return alias;
|
||||
}
|
||||
|
||||
function replaceAtMentionWithToken(fullMatch, mention, username) {
|
||||
let usernameLower = username.toLowerCase();
|
||||
|
||||
if (mentionExists(usernameLower)) {
|
||||
// Exact match
|
||||
const alias = addToken(usernameLower, mention, '');
|
||||
return alias;
|
||||
}
|
||||
|
||||
// Not an exact match, attempt to truncate any punctuation to see if we can find a user
|
||||
const originalUsername = usernameLower;
|
||||
|
||||
for (let c = usernameLower.length; c > 0; c--) {
|
||||
if (isPunctuation(usernameLower[c - 1])) {
|
||||
usernameLower = usernameLower.substring(0, c - 1);
|
||||
|
||||
if (mentionExists(usernameLower)) {
|
||||
const suffix = originalUsername.substr(c - 1);
|
||||
const alias = addToken(usernameLower, '@' + usernameLower);
|
||||
return alias + suffix;
|
||||
}
|
||||
} else {
|
||||
// If the last character is not punctuation, no point in going any further
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return fullMatch;
|
||||
}
|
||||
|
||||
let output = text;
|
||||
output = output.replace(/(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function escapeRegex(text) {
|
||||
return text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
function highlightCurrentMentions(text, tokens) {
|
||||
let output = text;
|
||||
|
||||
const mentionKeys = UserStore.getCurrentMentionKeys();
|
||||
|
||||
// look for any existing tokens which are self mentions and should be highlighted
|
||||
var newTokens = new Map();
|
||||
for (const [alias, token] of tokens) {
|
||||
if (mentionKeys.indexOf(token.originalText) !== -1) {
|
||||
const index = tokens.size + newTokens.size;
|
||||
const newAlias = `MM_SELFMENTION${index}`;
|
||||
|
||||
newTokens.set(newAlias, {
|
||||
value: `<span class='mention--highlight'>${alias}</span>`,
|
||||
originalText: token.originalText
|
||||
});
|
||||
output = output.replace(alias, newAlias);
|
||||
}
|
||||
}
|
||||
|
||||
// the new tokens are stashed in a separate map since we can't add objects to a map during iteration
|
||||
for (const newToken of newTokens) {
|
||||
tokens.set(newToken[0], newToken[1]);
|
||||
}
|
||||
|
||||
// look for self mentions in the text
|
||||
function replaceCurrentMentionWithToken(fullMatch, prefix, mention) {
|
||||
const index = tokens.size;
|
||||
const alias = `MM_SELFMENTION${index}`;
|
||||
|
||||
tokens.set(alias, {
|
||||
value: `<span class='mention--highlight'>${mention}</span>`,
|
||||
originalText: mention
|
||||
});
|
||||
|
||||
return prefix + alias;
|
||||
}
|
||||
|
||||
for (const mention of UserStore.getCurrentMentionKeys()) {
|
||||
output = output.replace(new RegExp(`(^|\\W)(${escapeRegex(mention)})\\b`, 'gi'), replaceCurrentMentionWithToken);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function autolinkHashtags(text, tokens) {
|
||||
let output = text;
|
||||
|
||||
var newTokens = new Map();
|
||||
for (const [alias, token] of tokens) {
|
||||
if (token.originalText.lastIndexOf('#', 0) === 0) {
|
||||
const index = tokens.size + newTokens.size;
|
||||
const newAlias = `MM_HASHTAG${index}`;
|
||||
|
||||
newTokens.set(newAlias, {
|
||||
value: `<a class='mention-link' href='#' data-hashtag='${token.originalText}'>${token.originalText}</a>`,
|
||||
originalText: token.originalText
|
||||
});
|
||||
|
||||
output = output.replace(alias, newAlias);
|
||||
}
|
||||
}
|
||||
|
||||
// the new tokens are stashed in a separate map since we can't add objects to a map during iteration
|
||||
for (const newToken of newTokens) {
|
||||
tokens.set(newToken[0], newToken[1]);
|
||||
}
|
||||
|
||||
// look for hashtags in the text
|
||||
function replaceHashtagWithToken(fullMatch, prefix, hashtag) {
|
||||
const index = tokens.size;
|
||||
const alias = `MM_HASHTAG${index}`;
|
||||
|
||||
let value = hashtag;
|
||||
|
||||
if (hashtag.length > Constants.MIN_HASHTAG_LINK_LENGTH) {
|
||||
value = `<a class='mention-link' href='#' data-hashtag='${hashtag}'>${hashtag}</a>`;
|
||||
}
|
||||
|
||||
tokens.set(alias, {
|
||||
value,
|
||||
originalText: hashtag
|
||||
});
|
||||
|
||||
return prefix + alias;
|
||||
}
|
||||
|
||||
return output.replace(/(^|\W)(#[a-zA-ZäöüÄÖÜß][a-zA-Z0-9äöüÄÖÜß.\-_]*)\b/g, replaceHashtagWithToken);
|
||||
}
|
||||
|
||||
const puncStart = /^[.,()&$!\[\]{}':;\\]+/;
|
||||
const puncEnd = /[.,()&$#!\[\]{}':;\\]+$/;
|
||||
|
||||
function parseSearchTerms(searchTerm) {
|
||||
let terms = [];
|
||||
|
||||
let termString = searchTerm;
|
||||
|
||||
while (termString) {
|
||||
let captured;
|
||||
|
||||
// check for a quoted string
|
||||
captured = (/^"(.*?)"/).exec(termString);
|
||||
if (captured) {
|
||||
termString = termString.substring(captured[0].length);
|
||||
terms.push(captured[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// check for a search flag (and don't add it to terms)
|
||||
captured = (/^(?:in|from|channel): ?\S+/).exec(termString);
|
||||
if (captured) {
|
||||
termString = termString.substring(captured[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
// capture any plain text up until the next quote or search flag
|
||||
captured = (/^.+?(?=\bin|\bfrom|\bchannel|"|$)/).exec(termString);
|
||||
if (captured) {
|
||||
termString = termString.substring(captured[0].length);
|
||||
|
||||
// break the text up into words based on how the server splits them in SqlPostStore.SearchPosts and then discard empty terms
|
||||
terms.push(...captured[0].split(/[ <>+\-\(\)\~\@]/).filter((term) => !!term));
|
||||
continue;
|
||||
}
|
||||
|
||||
// we should never reach this point since at least one of the regexes should match something in the remaining text
|
||||
throw new Error('Infinite loop in search term parsing: ' + termString);
|
||||
}
|
||||
|
||||
// remove punctuation from each term
|
||||
terms = terms.map((term) => term.replace(puncStart, '').replace(puncEnd, ''));
|
||||
|
||||
return terms;
|
||||
}
|
||||
|
||||
function convertSearchTermToRegex(term) {
|
||||
let pattern;
|
||||
if (term.endsWith('*')) {
|
||||
pattern = '\\b' + escapeRegex(term.substring(0, term.length - 1));
|
||||
} else {
|
||||
pattern = '\\b' + escapeRegex(term) + '\\b';
|
||||
}
|
||||
|
||||
return new RegExp(pattern, 'gi');
|
||||
}
|
||||
|
||||
function highlightSearchTerm(text, tokens, searchTerm) {
|
||||
const terms = parseSearchTerms(searchTerm);
|
||||
|
||||
if (terms.length === 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
let output = text;
|
||||
|
||||
function replaceSearchTermWithToken(word) {
|
||||
const index = tokens.size;
|
||||
const alias = `MM_SEARCHTERM${index}`;
|
||||
|
||||
tokens.set(alias, {
|
||||
value: `<span class='search-highlight'>${word}</span>`,
|
||||
originalText: word
|
||||
});
|
||||
|
||||
return alias;
|
||||
}
|
||||
|
||||
for (const term of terms) {
|
||||
// highlight existing tokens matching search terms
|
||||
var newTokens = new Map();
|
||||
for (const [alias, token] of tokens) {
|
||||
if (token.originalText === term.replace(/\*$/, '')) {
|
||||
const index = tokens.size + newTokens.size;
|
||||
const newAlias = `MM_SEARCHTERM${index}`;
|
||||
|
||||
newTokens.set(newAlias, {
|
||||
value: `<span class='search-highlight'>${alias}</span>`,
|
||||
originalText: token.originalText
|
||||
});
|
||||
|
||||
output = output.replace(alias, newAlias);
|
||||
}
|
||||
}
|
||||
|
||||
// the new tokens are stashed in a separate map since we can't add objects to a map during iteration
|
||||
for (const newToken of newTokens) {
|
||||
tokens.set(newToken[0], newToken[1]);
|
||||
}
|
||||
|
||||
output = output.replace(convertSearchTermToRegex(term), replaceSearchTermWithToken);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function replaceTokens(text, tokens) {
|
||||
let output = text;
|
||||
|
||||
// iterate backwards through the map so that we do replacement in the opposite order that we added tokens
|
||||
const aliases = [...tokens.keys()];
|
||||
for (let i = aliases.length - 1; i >= 0; i--) {
|
||||
const alias = aliases[i];
|
||||
const token = tokens.get(alias);
|
||||
output = output.replace(alias, token.value);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function replaceNewlines(text) {
|
||||
return text.replace(/\n/g, ' ');
|
||||
}
|
||||
|
||||
// A click handler that can be used with the results of TextFormatting.formatText to add default functionality
|
||||
// to clicked hashtags and @mentions.
|
||||
export function handleClick(e) {
|
||||
const mentionAttribute = e.target.getAttributeNode('data-mention');
|
||||
const hashtagAttribute = e.target.getAttributeNode('data-hashtag');
|
||||
|
||||
if (mentionAttribute) {
|
||||
Utils.searchForTerm(mentionAttribute.value);
|
||||
} else if (hashtagAttribute) {
|
||||
Utils.searchForTerm(hashtagAttribute.value);
|
||||
}
|
||||
}
|
||||
1411
webapp/utils/utils.jsx
Обычный файл
1411
webapp/utils/utils.jsx
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Ссылка в новой задаче
Block a user