Merge branch 'develop' into codex/cw-4998-disable-inbox
This commit is contained in:
@@ -63,6 +63,13 @@ const createMarkdownInstance = (linkify = true) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Help center article tables persist column widths as an internal
|
||||
// `<!--cw-colwidths:...-->` comment before the table. It exists only for the
|
||||
// editor's markdown round-trip and must never surface as text — markdown-it runs
|
||||
// with `html: false`, which would otherwise escape it into a visible comment in
|
||||
// rendered/plain output (e.g. dashboard search snippets). Strip it on the way in.
|
||||
const COLWIDTHS_MARKER_REGEX = /<!--cw-colwidths:[\d,]+-->\r?\n?/g;
|
||||
|
||||
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;
|
||||
@@ -75,7 +82,7 @@ class MessageFormatter {
|
||||
isAPrivateNote = false,
|
||||
linkify = true
|
||||
) {
|
||||
this.message = message || '';
|
||||
this.message = (message || '').replace(COLWIDTHS_MARKER_REGEX, '');
|
||||
this.isAPrivateNote = isAPrivateNote;
|
||||
this.isATweet = isATweet;
|
||||
this.linkify = linkify;
|
||||
|
||||
@@ -126,6 +126,16 @@ describe('#MessageFormatter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('help center table colwidth marker', () => {
|
||||
it('strips the internal colwidths marker from rendered output', () => {
|
||||
const message =
|
||||
'<!--cw-colwidths:120,200-->\n| A | B |\n| --- | --- |\n| 1 | 2 |';
|
||||
const formatter = new MessageFormatter(message);
|
||||
expect(formatter.formattedMessage).not.toContain('cw-colwidths');
|
||||
expect(formatter.plainText).not.toContain('cw-colwidths');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sanitize', () => {
|
||||
it('sanitizes markup and removes all unnecessary elements', () => {
|
||||
const message =
|
||||
|
||||
@@ -9,9 +9,32 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
|
||||
@embed_regexes ||= config.transform_values { |embed_config| Regexp.new(embed_config['regex']) }
|
||||
end
|
||||
|
||||
# Matches columnResizing({ cellMinWidth: 50 }) in @chatwoot/prosemirror-schema
|
||||
# so cells without an explicit colwidth render the same minimum here as in the editor.
|
||||
TABLE_CELL_MIN_WIDTH_PX = 50
|
||||
COLWIDTHS_COMMENT = /<!--cw-colwidths:([\d,]+)-->/
|
||||
|
||||
# The article editor serializes column widths as a `<!--cw-colwidths:...-->` HTML
|
||||
# comment immediately before each resized table. Capture it (emitting nothing) so the
|
||||
# next `table` can size itself; any other raw HTML keeps its default rendering.
|
||||
def html(node)
|
||||
match = node.string_content.match(COLWIDTHS_COMMENT)
|
||||
return super unless match
|
||||
|
||||
@pending_colwidths = match[1].split(',').map(&:to_i)
|
||||
end
|
||||
|
||||
def table(node)
|
||||
out('<div class="tableWrapper">')
|
||||
super
|
||||
widths = @pending_colwidths
|
||||
@pending_colwidths = nil
|
||||
|
||||
if sized_widths?(widths)
|
||||
out(table_wrapper_open(widths))
|
||||
out(inject_table_sizing(capture_html { super(node) }, widths))
|
||||
else
|
||||
out('<div class="tableWrapper">')
|
||||
super
|
||||
end
|
||||
out('</div>')
|
||||
end
|
||||
|
||||
@@ -47,6 +70,61 @@ class CustomMarkdownRenderer < CommonMarker::HtmlRenderer
|
||||
|
||||
private
|
||||
|
||||
def sized_widths?(widths)
|
||||
widths.is_a?(Array) && widths.any? { |w| w.to_i.positive? }
|
||||
end
|
||||
|
||||
def fully_sized?(widths)
|
||||
widths.all? { |w| w.to_i.positive? }
|
||||
end
|
||||
|
||||
# Fully-sized tables hug their exact width so the card doesn't trail empty space;
|
||||
# partial tables stay a plain full-width card so flexible columns can expand.
|
||||
def table_wrapper_open(widths)
|
||||
return '<div class="tableWrapper">' unless fully_sized?(widths)
|
||||
|
||||
%(<div class="tableWrapper" style="width: #{total_width(widths)}px; max-width: 100%;">)
|
||||
end
|
||||
|
||||
# Let the gem render the whole table, then splice a <colgroup> and sizing style
|
||||
# into the opening <table> tag. Delegating the row/cell/tbody/alignment markup to
|
||||
# super keeps this working across commonmarker upgrades.
|
||||
# `!important` overrides the portal's `[&_table]:!min-w-full` Tailwind rule.
|
||||
def inject_table_sizing(html, widths)
|
||||
opening = %(<table style="#{table_sizing_style(widths)}">\n#{colgroup_html(widths)})
|
||||
html.sub(/<table[^>]*>\n?/, opening)
|
||||
end
|
||||
|
||||
# Capture everything `super` writes by swapping the renderer's output buffer.
|
||||
def capture_html
|
||||
original = @stream
|
||||
@stream = StringIO.new(+'')
|
||||
yield
|
||||
@stream.string
|
||||
ensure
|
||||
@stream = original
|
||||
end
|
||||
|
||||
# Total table width: each column's saved width, or the cell min for unsized ones.
|
||||
def total_width(widths)
|
||||
widths.sum { |w| w.to_i.positive? ? w.to_i : TABLE_CELL_MIN_WIDTH_PX }
|
||||
end
|
||||
|
||||
# Fully sized → lock to the exact total (min-width too, so a narrow saved width
|
||||
# beats the portal's `[&_table]:!min-w-full`). Partial → `max(100%, total)` fills
|
||||
# the container (flexible columns) yet scrolls when the sized columns exceed it.
|
||||
def table_sizing_style(widths)
|
||||
total = total_width(widths)
|
||||
return "table-layout: fixed; min-width: max(100%, #{total}px) !important;" unless fully_sized?(widths)
|
||||
|
||||
"table-layout: fixed; width: #{total}px !important; min-width: #{total}px !important;"
|
||||
end
|
||||
|
||||
def colgroup_html(widths)
|
||||
cols = widths.map { |w| w.to_i.positive? ? %(<col style="width: #{w.to_i}px;">) : '<col>' }
|
||||
"<colgroup>#{cols.join}</colgroup>\n"
|
||||
end
|
||||
|
||||
def extract_image_width(src)
|
||||
query = URI.parse(src).query
|
||||
raw = query && CGI.parse(query)['cw_image_width']&.first
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"@amplitude/analytics-browser": "^2.11.10",
|
||||
"@breezystack/lamejs": "^1.2.7",
|
||||
"@chatwoot/ninja-keys": "1.2.3",
|
||||
"@chatwoot/prosemirror-schema": "1.3.17",
|
||||
"@chatwoot/prosemirror-schema": "1.3.19",
|
||||
"@chatwoot/utils": "^0.0.55",
|
||||
"@formkit/core": "^1.7.2",
|
||||
"@formkit/vue": "^1.7.2",
|
||||
|
||||
Generated
+5
-5
@@ -25,8 +25,8 @@ importers:
|
||||
specifier: 1.2.3
|
||||
version: 1.2.3
|
||||
'@chatwoot/prosemirror-schema':
|
||||
specifier: 1.3.17
|
||||
version: 1.3.17
|
||||
specifier: 1.3.19
|
||||
version: 1.3.19
|
||||
'@chatwoot/utils':
|
||||
specifier: ^0.0.55
|
||||
version: 0.0.55
|
||||
@@ -458,8 +458,8 @@ packages:
|
||||
'@chatwoot/ninja-keys@1.2.3':
|
||||
resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
|
||||
|
||||
'@chatwoot/prosemirror-schema@1.3.17':
|
||||
resolution: {integrity: sha512-n78ZfMIzSrylImIN5cjCeEdTJ8ub0JtCybwUlqFyOyLy3ZzAZpOHvCSo+w/KmV4dCgOH2mBmYlxBQ9Rww+e0Rw==}
|
||||
'@chatwoot/prosemirror-schema@1.3.19':
|
||||
resolution: {integrity: sha512-LbATIAeTzclvbIK6WjtrGUO37AtMWkzCJi+s/KpUIp81TORhc0fHnclGT3353AbxeH6dF51/4hFTTsmF+ziqGA==}
|
||||
|
||||
'@chatwoot/utils@0.0.55':
|
||||
resolution: {integrity: sha512-8G6HYQe1ZEYfJEsSYfDVvE+uhf98JDRjtGlpB+bzMko+yltbrk4yACSo/ImC3jSaJ6K8yPTSjJToSRmsQbL2iQ==}
|
||||
@@ -5128,7 +5128,7 @@ snapshots:
|
||||
hotkeys-js: 3.8.7
|
||||
lit: 2.2.6
|
||||
|
||||
'@chatwoot/prosemirror-schema@1.3.17':
|
||||
'@chatwoot/prosemirror-schema@1.3.19':
|
||||
dependencies:
|
||||
markdown-it-sup: 2.0.0
|
||||
prosemirror-commands: 1.7.1
|
||||
|
||||
@@ -258,6 +258,59 @@ describe CustomMarkdownRenderer do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#table' do
|
||||
def render_table(markdown)
|
||||
doc = CommonMarker.render_doc(markdown, :DEFAULT, [:table])
|
||||
described_class.new.render(doc)
|
||||
end
|
||||
|
||||
let(:plain_table) { "| A | B |\n| --- | --- |\n| 1 | 2 |\n" }
|
||||
|
||||
it 'renders a table without column widths when no marker is present' do
|
||||
output = render_table(plain_table)
|
||||
expect(output).to include('<div class="tableWrapper"><table>')
|
||||
expect(output).not_to include('colgroup')
|
||||
expect(output).not_to include('cw-colwidths')
|
||||
end
|
||||
|
||||
context 'when every column has a saved width' do
|
||||
it 'lays the table out at the total width with a sized colgroup' do
|
||||
output = render_table("<!--cw-colwidths:120,200-->\n#{plain_table}")
|
||||
# Wrapper hugs the table; min-width is set alongside width so a narrow saved width beats min-w-full.
|
||||
expect(output).to include('<div class="tableWrapper" style="width: 320px; max-width: 100%;">')
|
||||
expect(output).to include('<table style="table-layout: fixed; width: 320px !important; min-width: 320px !important;">')
|
||||
expect(output).to include('<colgroup><col style="width: 120px;"><col style="width: 200px;"></colgroup>')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only some columns have a saved width' do
|
||||
it 'fills the container so unsized columns stay flexible, floored at the sized total' do
|
||||
output = render_table("<!--cw-colwidths:150,0-->\n#{plain_table}")
|
||||
# max(100%, 200px): fills the container (flexible) but scrolls if the sized columns exceed it.
|
||||
expect(output).to include('table-layout: fixed; min-width: max(100%, 200px) !important;')
|
||||
expect(output).to include('<colgroup><col style="width: 150px;"><col></colgroup>')
|
||||
# No exact-width lock on the wrapper or table — the table must be free to expand.
|
||||
expect(output).to include('<div class="tableWrapper"><table')
|
||||
expect(output).not_to include('width: 200px !important')
|
||||
end
|
||||
end
|
||||
|
||||
it 'associates each marker with the table that follows it' do
|
||||
markdown = "#{plain_table}\n<!--cw-colwidths:150,250-->\n| P | Q |\n| --- | --- |\n| a | b |\n"
|
||||
output = render_table(markdown)
|
||||
|
||||
# First table has no marker and stays unsized; the marker applies to the second table.
|
||||
expect(output).to include('<div class="tableWrapper"><table>')
|
||||
expect(output).to include('width: 400px !important;')
|
||||
expect(output).to include('<col style="width: 250px;">')
|
||||
expect(output.scan('colgroup').length).to eq(2)
|
||||
end
|
||||
|
||||
it 'does not emit the marker comment into the rendered html' do
|
||||
expect(render_table("<!--cw-colwidths:120,200-->\n#{plain_table}")).not_to include('cw-colwidths')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#image' do
|
||||
it 'renders width in px with responsive cap and auto height' do
|
||||
markdown = ''
|
||||
|
||||
Reference in New Issue
Block a user