@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Display some invisible/nonprintable characters in diffs by default

Summary:
Ref T12822. Ref T2495. This is the good version of D20193.

Currently, we display various nonprintable characters (ZWS, nonbreaking space, various control characters) as themselves, so they're generally invisible.

In T12822, one user reports that all their engineers frequently type ZWS characters into source somehow? I don't really believe this (??), and this should be fixed in lint.

That said, the only real reason not to show these weird characters in a special way was that it would break copy/paste: if we render ZWS as "🐑", and a user copy-pastes the line including the ZWS, they'll get a sheep.

At least, they would have, until D20191. Now that this whole thing is end-to-end Javascript magic, we can copy whatever we want.

In particular, we can render any character `X` as `<span data-copy-text="Y">X</span>`, and then copy "Y" instead of "X" when the user copies the node. Limitations:

- If users select only "X", they'll get "X" on their clipboard. This seems fine. If you're selecting our ZWS marker *only*, you probably want to copy it?
- If "X" is more than one character long, users will get the full "Y" if they select any part of "X". At least here, this only matters when "X" is several spaces and "Y" is a tab. This also seems fine.
- We have to be kind of careful because this approach involves editing an HTML blob directly. However, we already do that elsewhere and this isn't really too hard to get right.

With those tools in hand:

- Replace "\t" (raw text / what gets copied) with the number of spaces to the next tab stop for display.
- Replace ZWS and NBSP (raw text) with a special marker for display.
- Replace control characters 0x00-0x19 and 0x7F, except for "\t", "\r", and "\n", with the special unicode "control character pictures" reserved for this purpose.

Test Plan:
- Generated and viewed a file like this one:

{F6220422}

- Copied text out of it, got authentic raw original source text instead of displayed text.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T12822, T2495

Differential Revision: https://secure.phabricator.com/D20194

+221 -24
+13 -13
resources/celerity/map.php
··· 9 9 'names' => array( 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 - 'core.pkg.css' => '261ee8cf', 13 - 'core.pkg.js' => 'e368deda', 12 + 'core.pkg.css' => 'e3c1a8f2', 13 + 'core.pkg.js' => '2cda17a4', 14 14 'differential.pkg.css' => '249b542d', 15 15 'differential.pkg.js' => '53f8d00c', 16 16 'diffusion.pkg.css' => '42c75c37', ··· 112 112 'rsrc/css/application/uiexample/example.css' => 'b4795059', 113 113 'rsrc/css/core/core.css' => '1b29ed61', 114 114 'rsrc/css/core/remarkup.css' => '9e627d41', 115 - 'rsrc/css/core/syntax.css' => '8a16f91b', 115 + 'rsrc/css/core/syntax.css' => '4234f572', 116 116 'rsrc/css/core/z-index.css' => '99c0f5eb', 117 117 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0', 118 118 'rsrc/css/font/font-awesome.css' => '3883938a', ··· 473 473 'rsrc/js/core/behavior-linked-container.js' => '74446546', 474 474 'rsrc/js/core/behavior-more.js' => '506aa3f4', 475 475 'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a', 476 - 'rsrc/js/core/behavior-oncopy.js' => 'de59bf15', 476 + 'rsrc/js/core/behavior-oncopy.js' => 'ff7b3f22', 477 477 'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949', 478 478 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f', 479 479 'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f', ··· 636 636 'javelin-behavior-phabricator-nav' => 'f166c949', 637 637 'javelin-behavior-phabricator-notification-example' => '29819b75', 638 638 'javelin-behavior-phabricator-object-selector' => 'a4af0b4a', 639 - 'javelin-behavior-phabricator-oncopy' => 'de59bf15', 639 + 'javelin-behavior-phabricator-oncopy' => 'ff7b3f22', 640 640 'javelin-behavior-phabricator-remarkup-assist' => '2f80333f', 641 641 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', 642 642 'javelin-behavior-phabricator-search-typeahead' => '1cb7d027', ··· 878 878 'sprite-login-css' => '18b368a6', 879 879 'sprite-tokens-css' => 'f1896dc5', 880 880 'syntax-default-css' => '055fc231', 881 - 'syntax-highlighting-css' => '8a16f91b', 881 + 'syntax-highlighting-css' => '4234f572', 882 882 'tokens-css' => 'ce5a50bd', 883 883 'typeahead-browse-css' => 'b7ed02d2', 884 884 'unhandled-exception-css' => '9ecfc00d', ··· 1222 1222 'javelin-behavior', 1223 1223 'javelin-uri', 1224 1224 ), 1225 + '4234f572' => array( 1226 + 'syntax-default-css', 1227 + ), 1225 1228 '42c7a5a7' => array( 1226 1229 'javelin-install', 1227 1230 'javelin-dom', ··· 1579 1582 'javelin-util', 1580 1583 'javelin-stratcom', 1581 1584 'javelin-install', 1582 - ), 1583 - '8a16f91b' => array( 1584 - 'syntax-default-css', 1585 1585 ), 1586 1586 '8ac32fd9' => array( 1587 1587 'javelin-behavior', ··· 2010 2010 'javelin-uri', 2011 2011 'phabricator-notification', 2012 2012 ), 2013 - 'de59bf15' => array( 2014 - 'javelin-behavior', 2015 - 'javelin-dom', 2016 - ), 2017 2013 'dfa1d313' => array( 2018 2014 'javelin-behavior', 2019 2015 'javelin-dom', ··· 2146 2142 'ff688a7a' => array( 2147 2143 'owners-path-editor', 2148 2144 'javelin-behavior', 2145 + ), 2146 + 'ff7b3f22' => array( 2147 + 'javelin-behavior', 2148 + 'javelin-dom', 2149 2149 ), 2150 2150 ), 2151 2151 'packages' => array(
+187 -3
src/applications/differential/parser/DifferentialChangesetParser.php
··· 189 189 return $this; 190 190 } 191 191 192 - const CACHE_VERSION = 13; 192 + const CACHE_VERSION = 14; 193 193 const CACHE_MAX_SIZE = 8e6; 194 194 195 195 const ATTR_GENERATED = 'attr:generated'; ··· 568 568 private function applyIntraline(&$render, $intra, $corpus) { 569 569 570 570 foreach ($render as $key => $text) { 571 + $result = $text; 572 + 571 573 if (isset($intra[$key])) { 572 - $render[$key] = ArcanistDiffUtils::applyIntralineDiff( 573 - $text, 574 + $result = ArcanistDiffUtils::applyIntralineDiff( 575 + $result, 574 576 $intra[$key]); 575 577 } 578 + 579 + $result = $this->adjustRenderedLineForDisplay($result); 580 + 581 + $render[$key] = $result; 576 582 } 577 583 } 578 584 ··· 1415 1421 $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); 1416 1422 } 1417 1423 1424 + private function adjustRenderedLineForDisplay($line) { 1425 + // IMPORTANT: We're using "str_replace()" against raw HTML here, which can 1426 + // easily become unsafe. The input HTML has already had syntax highlighting 1427 + // and intraline diff highlighting applied, so it's full of "<span />" tags. 1428 + 1429 + static $search; 1430 + static $replace; 1431 + if ($search === null) { 1432 + $rules = $this->newSuspiciousCharacterRules(); 1433 + 1434 + $map = array(); 1435 + foreach ($rules as $key => $spec) { 1436 + $tag = phutil_tag( 1437 + 'span', 1438 + array( 1439 + 'data-copy-text' => $key, 1440 + 'class' => $spec['class'], 1441 + 'title' => $spec['title'], 1442 + ), 1443 + $spec['replacement']); 1444 + $map[$key] = phutil_string_cast($tag); 1445 + } 1446 + 1447 + $search = array_keys($map); 1448 + $replace = array_values($map); 1449 + } 1450 + 1451 + $is_html = false; 1452 + if ($line instanceof PhutilSafeHTML) { 1453 + $is_html = true; 1454 + $line = hsprintf('%s', $line); 1455 + } 1456 + 1457 + $line = phutil_string_cast($line); 1458 + 1459 + if (strpos($line, "\t") !== false) { 1460 + $line = $this->replaceTabsWithSpaces($line); 1461 + } 1462 + $line = str_replace($search, $replace, $line); 1463 + 1464 + if ($is_html) { 1465 + $line = phutil_safe_html($line); 1466 + } 1467 + 1468 + return $line; 1469 + } 1470 + 1471 + private function newSuspiciousCharacterRules() { 1472 + // The "title" attributes are cached in the database, so they're 1473 + // intentionally not wrapped in "pht(...)". 1474 + 1475 + $rules = array( 1476 + "\xE2\x80\x8B" => array( 1477 + 'title' => 'ZWS', 1478 + 'class' => 'suspicious-character', 1479 + 'replacement' => '!', 1480 + ), 1481 + "\xC2\xA0" => array( 1482 + 'title' => 'NBSP', 1483 + 'class' => 'suspicious-character', 1484 + 'replacement' => '!', 1485 + ), 1486 + "\x7F" => array( 1487 + 'title' => 'DEL (0x7F)', 1488 + 'class' => 'suspicious-character', 1489 + 'replacement' => "\xE2\x90\xA1", 1490 + ), 1491 + ); 1492 + 1493 + // Unicode defines special pictures for the control characters in the 1494 + // range between "0x00" and "0x1F". 1495 + 1496 + $control = array( 1497 + 'NULL', 1498 + 'SOH', 1499 + 'STX', 1500 + 'ETX', 1501 + 'EOT', 1502 + 'ENQ', 1503 + 'ACK', 1504 + 'BEL', 1505 + 'BS', 1506 + null, // "\t" Tab 1507 + null, // "\n" New Line 1508 + 'VT', 1509 + 'FF', 1510 + null, // "\r" Carriage Return, 1511 + 'SO', 1512 + 'SI', 1513 + 'DLE', 1514 + 'DC1', 1515 + 'DC2', 1516 + 'DC3', 1517 + 'DC4', 1518 + 'NAK', 1519 + 'SYN', 1520 + 'ETB', 1521 + 'CAN', 1522 + 'EM', 1523 + 'SUB', 1524 + 'ESC', 1525 + 'FS', 1526 + 'GS', 1527 + 'RS', 1528 + 'US', 1529 + ); 1530 + 1531 + foreach ($control as $idx => $label) { 1532 + if ($label === null) { 1533 + continue; 1534 + } 1535 + 1536 + $rules[chr($idx)] = array( 1537 + 'title' => sprintf('%s (0x%02X)', $label, $idx), 1538 + 'class' => 'suspicious-character', 1539 + 'replacement' => "\xE2\x90".chr(0x80 + $idx), 1540 + ); 1541 + } 1542 + 1543 + return $rules; 1544 + } 1545 + 1546 + private function replaceTabsWithSpaces($line) { 1547 + // TODO: This should be flexible, eventually. 1548 + $tab_width = 2; 1549 + 1550 + static $tags; 1551 + if ($tags === null) { 1552 + $tags = array(); 1553 + for ($ii = 1; $ii <= $tab_width; $ii++) { 1554 + $tag = phutil_tag( 1555 + 'span', 1556 + array( 1557 + 'data-copy-text' => "\t", 1558 + ), 1559 + str_repeat(' ', $ii)); 1560 + $tag = phutil_string_cast($tag); 1561 + $tags[$ii] = $tag; 1562 + } 1563 + } 1564 + 1565 + // If the line is particularly long, don't try to vectorize it. Use a 1566 + // faster approximation of the correct tabstop expansion instead. This 1567 + // usually still arrives at the right result. 1568 + if (strlen($line) > 256) { 1569 + return str_replace("\t", $tags[$tab_width], $line); 1570 + } 1571 + 1572 + $line = phutil_utf8v_combined($line); 1573 + $in_tag = false; 1574 + $pos = 0; 1575 + foreach ($line as $key => $char) { 1576 + if ($char === '<') { 1577 + $in_tag = true; 1578 + continue; 1579 + } 1580 + 1581 + if ($char === '>') { 1582 + $in_tag = false; 1583 + continue; 1584 + } 1585 + 1586 + if ($in_tag) { 1587 + continue; 1588 + } 1589 + 1590 + if ($char === "\t") { 1591 + $count = $tab_width - ($pos % $tab_width); 1592 + $pos += $count; 1593 + $line[$key] = $tags[$count]; 1594 + continue; 1595 + } 1596 + 1597 + $pos++; 1598 + } 1599 + 1600 + return implode('', $line); 1601 + } 1418 1602 1419 1603 }
+7 -7
src/applications/differential/render/DifferentialChangesetRenderer.php
··· 363 363 $undershield = $this->renderUndershieldHeader(); 364 364 } 365 365 366 - $result = $notice.$props.$undershield.$content; 367 - 368 - // TODO: Let the user customize their tab width / display style. 369 - // TODO: We should possibly post-process "\r" as well. 370 - // TODO: Both these steps should happen earlier. 371 - $result = str_replace("\t", ' ', $result); 366 + $result = array( 367 + $notice, 368 + $props, 369 + $undershield, 370 + $content, 371 + ); 372 372 373 - return phutil_safe_html($result); 373 + return hsprintf('%s', $result); 374 374 } 375 375 376 376 abstract public function isOneUpRenderer();
+1 -1
src/applications/differential/render/DifferentialChangesetTestRenderer.php
··· 131 131 } 132 132 133 133 $out = implode("\n", $out)."\n"; 134 - return $out; 134 + return phutil_safe_html($out); 135 135 } 136 136 137 137
+6
webroot/rsrc/css/core/syntax.css
··· 29 29 color: #222222; 30 30 background: #dddddd; 31 31 } 32 + 33 + .suspicious-character { 34 + background: #ff7700; 35 + color: #ffffff; 36 + cursor: default; 37 + }
+7
webroot/rsrc/js/core/behavior-oncopy.js
··· 271 271 // Otherwise, fall through and extract this node's text normally. 272 272 } 273 273 274 + if (node.getAttribute) { 275 + var copy_text = node.getAttribute('data-copy-text'); 276 + if (copy_text) { 277 + return copy_text; 278 + } 279 + } 280 + 274 281 if (!node.childNodes || !node.childNodes.length) { 275 282 return node.textContent; 276 283 }