Files
gochat/frontend/app/javascript/shared/helpers/MessageFormatter.js
T
rogee 321c61aaae Vendor Chatwoot Vue 3 frontend into frontend/
Copy the Chatwoot (v4.14.0) frontend runnable subset into frontend/ for
customization:
- app/javascript/ (Vue SPA: dashboard, widget, sdk, portal, superadmin)
- app/views/ (ERB templates for vite-plugin-ruby entrypoint resolution)
- app/helpers/, app/assets/ (Rails view helpers, static assets)
- enterprise/ (Enterprise edition frontend overlay)
- config/vite.json, vite.config.ts, bin/vite (Vite-Rails toolchain)
- package.json, pnpm-lock.yaml, tailwind/postcss/eslint configs
- Gemfile, Gemfile.lock (vite_rails gem for bin/vite binstub)

Excluded Rails backend: controllers, models, services, jobs, mailers,
policies, db, lib, spec, public, node_modules.

Update references to the new frontend location:
- .gitignore: exclude frontend build artifacts (node_modules, tmp, packs),
  keep frontend/bin/ and frontend/vendor/ via negation
- backend/scripts/parity_frontend_smoke.sh: CHATWOOT_DIR default
  reference/chatwoot -> ../frontend
- backend/scripts/parity_frontend_browser_smoke.mjs: same default update
- AGENTS.md: add frontend section with Rails/Vite coupling notes
- README: architecture tree includes frontend/
2026-07-07 14:56:01 +08:00

105 lines
2.7 KiB
JavaScript

import mila from 'markdown-it-link-attributes';
import mentionPlugin from './markdownIt/link';
import MarkdownIt from 'markdown-it';
const setImageHeight = inlineToken => {
const imgSrc = inlineToken.attrGet('src');
if (!imgSrc) return;
const url = new URL(imgSrc);
const height = url.searchParams.get('cw_image_height');
if (!height) return;
inlineToken.attrSet('style', `height: ${height};`);
};
const processInlineToken = blockToken => {
blockToken.children.forEach(inlineToken => {
if (inlineToken.type === 'image') {
setImageHeight(inlineToken);
}
});
};
const imgResizeManager = md => {
// Custom rule for image resize in markdown
// If the image url has a query param cw_image_height, then add a style attribute to the image
md.core.ruler.after('inline', 'add-image-height', state => {
state.tokens.forEach(blockToken => {
if (blockToken.type === 'inline') {
processInlineToken(blockToken);
}
});
});
};
const createMarkdownInstance = (linkify = true) => {
return MarkdownIt({
html: false,
xhtmlOut: true,
breaks: true,
langPrefix: 'language-',
linkify,
typographer: true,
quotes: '\u201c\u201d\u2018\u2019',
maxNesting: 20,
})
.disable(['lheading'])
.use(mentionPlugin)
.use(imgResizeManager)
.use(mila, {
attrs: {
class: 'link',
rel: 'noreferrer noopener nofollow',
target: '_blank',
},
});
};
const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g;
const TWITTER_USERNAME_REPLACEMENT = '$1[@$2](http://twitter.com/$2)';
const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g;
const TWITTER_HASH_REPLACEMENT = '$1[#$2](https://twitter.com/hashtag/$2)';
class MessageFormatter {
constructor(
message,
isATweet = false,
isAPrivateNote = false,
linkify = true
) {
this.message = message || '';
this.isAPrivateNote = isAPrivateNote;
this.isATweet = isATweet;
this.linkify = linkify;
this.md = createMarkdownInstance(linkify);
}
formatMessage() {
let updatedMessage = this.message;
if (this.isATweet && !this.isAPrivateNote) {
updatedMessage = updatedMessage.replace(
TWITTER_USERNAME_REGEX,
TWITTER_USERNAME_REPLACEMENT
);
updatedMessage = updatedMessage.replace(
TWITTER_HASH_REGEX,
TWITTER_HASH_REPLACEMENT
);
}
return this.md.render(updatedMessage);
}
get formattedMessage() {
return this.formatMessage();
}
get plainText() {
const strippedOutHtml = new DOMParser().parseFromString(
this.formattedMessage,
'text/html'
);
return strippedOutHtml.body.textContent || '';
}
}
export default MessageFormatter;