Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions MacDown/Code/Document/MPDocument.m
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,6 @@ static void (^MPGetPreviewLoadingCompletionHandler(MPDocument *doc))()
};
}


/**
* Issue #436: Scans a single line for a fenced-code-block marker (a run of 3+ backticks or
* tildes, allowing 0-3 leading spaces). Returns YES and reports the marker character, its
Expand Down Expand Up @@ -675,7 +674,6 @@ - (NSUInteger)mathJaxRenderGeneration
return _mathJaxRenderGeneration;
}


#pragma mark - Override

- (instancetype)init
Expand Down Expand Up @@ -2167,6 +2165,7 @@ - (void)renderer:(MPRenderer *)renderer didProduceHTMLOutput:(NSString *)html
@" body.innerHTML = html;"
@" if(window.Prism){Prism.highlightAll();}"
@" if(typeof window.macdownInitTaskList==='function'){window.macdownInitTaskList();}"
@" if(typeof window.macdownInitTableResize==='function'){window.macdownInitTableResize();}"
@" if(window.MathJax&&MathJax.Hub){"
@" MathJax.Hub.Queue(['Typeset',MathJax.Hub]);"
@" MathJax.Hub.Queue(function(){"
Expand Down Expand Up @@ -4715,6 +4714,19 @@ - (void)document:(NSDocument *)doc didPrint:(BOOL)ok context:(void *)context

#pragma mark - Interactive Checkbox Support (Issue #269)

- (NSDictionary<NSString *, NSString *> *)queryItemsByNameForURL:(NSURL *)url
{
NSURLComponents *components =
[NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
NSMutableDictionary *items = [NSMutableDictionary dictionary];
for (NSURLQueryItem *item in components.queryItems)
{
if (item.name.length && item.value)
items[item.name] = item.value;
}
return items;
}

/**
* Handle the checkbox toggle URL from the preview.
* URL format: x-macdown-checkbox://toggle/<index>
Expand All @@ -4724,17 +4736,7 @@ - (void)handleCheckboxToggle:(NSURL *)url
if (![url.host isEqualToString:@"toggle"])
return;

NSURLComponents *components =
[NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
NSString *token = nil;
for (NSURLQueryItem *item in components.queryItems)
{
if ([item.name isEqualToString:@"token"])
{
token = item.value;
break;
}
}
NSString *token = [self queryItemsByNameForURL:url][@"token"];
if (!token.length
|| ![token isEqualToString:self.renderer.checkboxBridgeToken])
{
Expand Down
12 changes: 9 additions & 3 deletions MacDown/Code/Document/MPRenderer.m
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,11 @@ NS_INLINE void MPFreeHTMLRenderer(hoedown_renderer *htmlRenderer)
NS_INLINE NSString *MPPreviewHeadTags(NSString *checkboxBridgeToken)
{
NSString *csp = MPEscapeHTMLAttribute(MPPreviewContentSecurityPolicy());
NSString *token = MPEscapeHTMLAttribute(checkboxBridgeToken);
NSString *checkboxToken = MPEscapeHTMLAttribute(checkboxBridgeToken);
return [NSString stringWithFormat:
@"<meta http-equiv=\"Content-Security-Policy\" content=\"%@\">\n"
"<meta name=\"macdown-checkbox-token\" content=\"%@\">",
csp, token];
csp, checkboxToken];
}


Expand Down Expand Up @@ -686,6 +686,10 @@ - (NSArray *)scripts
NSURL *url = MPExtensionURL(@"tasklist", @"js");
[scripts addObject:[MPScript javaScriptWithURL:url]];
}
{
NSURL *url = MPExtensionURL(@"table-resize", @"js");
[scripts addObject:[MPScript javaScriptWithURL:url]];
}
if ([d rendererHasSyntaxHighlighting:self])
{
[scripts addObjectsFromArray:self.prismScripts];
Expand Down Expand Up @@ -825,13 +829,15 @@ - (void)render
id<MPRendererDelegate> delegate = self.delegate;

NSString *body = self.currentHtml;
NSString *previewBody = body ?: @"";

NSString *title = [self.dataSource rendererHTMLTitle:self];
if (!self.checkboxBridgeToken.length)
self.checkboxBridgeToken = NSUUID.UUID.UUIDString;
NSString *headTags = MPPreviewHeadTags(self.checkboxBridgeToken);
NSString *html = MPGetHTML(
title, headTags, body, self.stylesheets, MPAssetFullLink,
title, headTags, previewBody,
self.stylesheets, MPAssetFullLink,
self.scripts, MPAssetFullLink);

// Issue #110 / #318: Apply cache-busting version stamps to local resource
Expand Down
45 changes: 45 additions & 0 deletions MacDown/Resources/Extensions/export.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ td, th {
overflow-wrap: break-word;
}

/* Table headers - keep short labels such as "v1-A" on one line */
th {
white-space: nowrap;
word-break: normal;
overflow-wrap: normal;
}

/* Wide tables - give every theme a horizontal scroll container so columns keep
* their natural width instead of compressing to the pane width (Issue #432).
*
Expand All @@ -52,6 +59,44 @@ td, th {
}
}

.macdown-resizable-table {
display: table;
table-layout: fixed;
width: max-content;
max-width: none;
}

.macdown-table-resizable {
position: relative;
padding-right: 14px;
}

.macdown-table-resize-handle {
bottom: 0;
cursor: col-resize;
position: absolute;
right: -4px;
top: 0;
width: 8px;
z-index: 2;
}

.macdown-table-resize-handle::after {
background: currentColor;
bottom: 4px;
content: "";
opacity: 0.25;
position: absolute;
right: 3px;
top: 4px;
width: 1px;
}

.macdown-table-resize-handle:hover::after,
.macdown-table-resizing .macdown-table-resize-handle::after {
opacity: 0.7;
}

/* Blockquotes - ensure quoted content wraps */
blockquote {
word-break: break-word;
Expand Down
174 changes: 174 additions & 0 deletions MacDown/Resources/Extensions/table-resize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Live-preview Markdown table column resizing.
*
* Widths are ephemeral: kept only in this script's in-memory sessionWidths
* map, never written back to the document or persisted to disk. They survive
* incremental preview updates (typing) because the preview's JS context
* persists across DOM replacement, but reset on a full preview reload.
* This script only runs in the editor preview; exports do not include it.
*/
(function () {
var MIN_WIDTH = 48;
var resizing = null;
var sessionWidths = {};

function headerText(table) {
var cells = table.querySelectorAll('thead th');
if (!cells.length) {
cells = table.querySelectorAll('tr:first-child th, tr:first-child td');
}
var parts = [];
for (var i = 0; i < cells.length; i++) {
parts.push((cells[i].textContent || '').replace(/\s+/g, ' ').trim());
}
return parts.join('|');
}

function hashString(value) {
var hash = 2166136261;
for (var i = 0; i < value.length; i++) {
hash ^= value.charCodeAt(i);
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return (hash >>> 0).toString(16);
}

function tableKey(table, index) {
return index + ':' + hashString(headerText(table));
}

function ensureColgroup(table, columnCount) {
var colgroup = table.querySelector('colgroup');
if (!colgroup) {
colgroup = document.createElement('colgroup');
table.insertBefore(colgroup, table.firstChild);
}
while (colgroup.children.length < columnCount) {
colgroup.appendChild(document.createElement('col'));
}
while (colgroup.children.length > columnCount) {
colgroup.removeChild(colgroup.lastChild);
}
return colgroup;
}

function setColumnWidth(col, width) {
col.style.width = Math.max(MIN_WIDTH, Math.round(width)) + 'px';
}

function headerCells(table) {
var cells = table.querySelectorAll('thead th');
if (cells.length) {
return cells;
}
return table.querySelectorAll('tr:first-child th, tr:first-child td');
}

function teardownHandles(table) {
var oldHandles = table.querySelectorAll('.macdown-table-resize-handle');
for (var i = 0; i < oldHandles.length; i++) {
oldHandles[i].parentNode.removeChild(oldHandles[i]);
}
var oldCells = table.querySelectorAll('.macdown-table-resizable');
for (var j = 0; j < oldCells.length; j++) {
oldCells[j].classList.remove('macdown-table-resizable');
}
}

function initTable(table, index) {
var cells = headerCells(table);
if (!cells.length) {
return;
}

teardownHandles(table);

var key = tableKey(table, index);
table.setAttribute('data-macdown-table-key', key);
table.classList.add('macdown-resizable-table');

var colgroup = ensureColgroup(table, cells.length);
var saved = sessionWidths[key] || {};
for (var i = 0; i < cells.length; i++) {
var savedWidth = saved[String(i)];
if (savedWidth !== undefined && savedWidth !== null) {
setColumnWidth(colgroup.children[i], savedWidth);
}
}

for (var column = 0; column < cells.length; column++) {
(function (cell, columnIndex) {
cell.classList.add('macdown-table-resizable');
var handle = document.createElement('span');
handle.className = 'macdown-table-resize-handle';
handle.setAttribute('role', 'separator');
handle.setAttribute('aria-orientation', 'vertical');
handle.setAttribute('title', 'Resize column');

handle.addEventListener('mousedown', function (event) {
event.preventDefault();
event.stopPropagation();
var col = colgroup.children[columnIndex];
var rect = cell.getBoundingClientRect();
resizing = {
table: key,
column: columnIndex,
col: col,
startX: event.clientX,
startWidth: parseFloat(col.style.width) || rect.width
};
document.documentElement.classList.add('macdown-table-resizing');
});

handle.addEventListener('dblclick', function (event) {
event.preventDefault();
event.stopPropagation();
colgroup.children[columnIndex].style.width = '';
var tableWidths = sessionWidths[key];
if (tableWidths) {
delete tableWidths[String(columnIndex)];
if (!Object.keys(tableWidths).length) {
delete sessionWidths[key];
}
}
});

cell.appendChild(handle);
})(cells[column], column);
}
}

document.addEventListener('mousemove', function (event) {
if (!resizing) {
return;
}
var width = Math.max(MIN_WIDTH, resizing.startWidth + event.clientX - resizing.startX);
setColumnWidth(resizing.col, width);
});

document.addEventListener('mouseup', function () {
if (!resizing) {
return;
}
var width = parseFloat(resizing.col.style.width);
if (isFinite(width)) {
var tableWidths = sessionWidths[resizing.table];
if (!tableWidths) {
tableWidths = {};
sessionWidths[resizing.table] = tableWidths;
}
tableWidths[String(resizing.column)] = width;
}
resizing = null;
document.documentElement.classList.remove('macdown-table-resizing');
});

window.macdownInitTableResize = function () {
var tables = document.querySelectorAll('table');
for (var i = 0; i < tables.length; i++) {
initTable(tables[i], i);
}
};

window.macdownInitTableResize();
})();
13 changes: 13 additions & 0 deletions MacDownTests/MPHTMLExportTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,19 @@ - (void)testExportCSSContainsListAndTableBreaking
@"Should target th elements");
}

- (void)testExportCSSKeepsTableHeadersOnOneLine
{
NSString *cssContent = [self exportCSSContent];
XCTAssertNotNil(cssContent, @"export.css should have content");

XCTAssertTrue([cssContent containsString:@"white-space: nowrap"],
@"Table headers should not wrap short phase labels");
XCTAssertTrue([cssContent containsString:@"word-break: normal"],
@"Table headers should preserve normal word breaking");
XCTAssertTrue([cssContent containsString:@"overflow-x: auto"],
@"Wide tables should scroll horizontally instead of squeezing columns");
}

- (void)testExportCSSContainsBlockquoteAndDescriptionBreaking
{
NSString *cssContent = [self exportCSSContent];
Expand Down
8 changes: 8 additions & 0 deletions MacDownTests/MPRendererEdgeCaseTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ - (void)testPreviewRenderIncludesContentSecurityPolicyAndCheckboxToken
@"CSP should whitelist only bundled scripts and the MathJax CDN");
XCTAssertTrue([html containsString:@"name=\"macdown-checkbox-token\""],
@"Preview HTML should include a checkbox bridge token");
XCTAssertTrue([html containsString:@"table-resize.js"],
@"Preview HTML should include live table resizing behavior");
XCTAssertFalse([html containsString:@"name=\"macdown-table-layout-token\""],
@"Ephemeral table resizing uses no native bridge token");
XCTAssertFalse([html containsString:@"id=\"macdown-table-layouts\""],
@"Ephemeral table resizing uses no injected layout data");
XCTAssertTrue(self.renderer.checkboxBridgeToken.length > 0,
@"Renderer should expose the active checkbox bridge token");
}
Expand Down Expand Up @@ -212,6 +218,8 @@ - (void)testHTMLExportDoesNotIncludePreviewOnlySecurityMetaTags
@"Preview-only CSP should not be embedded into exports");
XCTAssertFalse([html containsString:@"macdown-checkbox-token"],
@"Preview-only checkbox tokens should not leak into exports");
XCTAssertFalse([html containsString:@"table-resize.js"],
@"Preview-only table resizing script should not leak into exports");
}


Expand Down
Loading