diff --git a/packages/scratch-gui/src/containers/ruby-tab.jsx b/packages/scratch-gui/src/containers/ruby-tab.jsx index 8a8a3b91fee..03b75e411fc 100644 --- a/packages/scratch-gui/src/containers/ruby-tab.jsx +++ b/packages/scratch-gui/src/containers/ruby-tab.jsx @@ -136,6 +136,15 @@ const RubyTab = (props) => { const urlRubyMode = getUrlParams().rubyMode; if (urlRubyMode === 'furigana') return true; if (urlRubyMode === 'ruby' || urlRubyMode === 'dncl') return false; + // DNCL mode shows Japanese pseudo-code, not Ruby — furigana annotations + // are meaningless and visually distracting in that view. Force off. + if ( + typeof window !== 'undefined' && + window.localStorage && + window.localStorage.getItem(DNCL_MODE_KEY) === 'true' + ) { + return false; + } return loadBool(FURIGANA_ENABLED_KEY, true); }); const [autoCorrectEnabled, setAutoCorrectEnabled] = useState(() => loadBool(AUTO_CORRECT_ENABLED_KEY, true)); @@ -251,6 +260,9 @@ const RubyTab = (props) => { const renderFurigana = () => { if (!editorRef.current || !monacoRef.current) return; + // Furigana annotations target Ruby source; in DNCL mode the editor + // shows Japanese pseudo-code, so suppress rendering entirely. + if (dnclModeRef.current) return; const code = editorRef.current.getValue() || ''; const prism = getPrism(); if (prism) { @@ -262,6 +274,7 @@ const RubyTab = (props) => { } else { loadPrism().then((loadedPrism) => { if (!furiganaEnabledRef.current) return; + if (dnclModeRef.current) return; if (!editorRef.current || !monacoRef.current) return; const currentCode = editorRef.current.getValue() || ''; const t0 = performance.now(); @@ -280,7 +293,7 @@ const RubyTab = (props) => { const delay = Math.max(50, furiganaLastMsRef.current * 2); furiganaDebounceTimerRef.current = setTimeout(() => { furiganaDebounceTimerRef.current = null; - if (furiganaEnabledRef.current) { + if (furiganaEnabledRef.current && !dnclModeRef.current) { renderFurigana(); } }, delay); @@ -639,6 +652,15 @@ const RubyTab = (props) => { isModeSwitchRef.current = false; setDnclMode(enabling); onSetDnclMode(enabling); + + // Furigana is meaningless in DNCL view (Japanese pseudo-code, not Ruby). + // Clear annotations on enable; restore them on disable when the user + // had furigana on. + if (enabling) { + furiganaRendererRef.current?.clear(editorRef.current); + } else if (furiganaEnabledRef.current) { + renderFurigana(); + } }, [vm, rubyCode.target, intl, rubyVersion, dnclValidationErrorMessage, onSetDnclMode]); const handleToggleFurigana = useCallback(() => { diff --git a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/ast-handlers/control-flow.js b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/ast-handlers/control-flow.js index 75b3ba248d3..83c021be6c9 100644 --- a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/ast-handlers/control-flow.js +++ b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/ast-handlers/control-flow.js @@ -68,7 +68,7 @@ const ControlFlowHandlers = { comment.minimized = true; } } else { - const commentId = this._createComment(commentText, b.id, 0, 0, true); + const commentId = this._createComment(commentText, b.id); b.comment = commentId; } }); @@ -86,7 +86,7 @@ const ControlFlowHandlers = { comment.minimized = true; } } else { - const commentId = this._createComment(commentText, block.id, 0, 0, true); + const commentId = this._createComment(commentText, block.id); block.comment = commentId; } } else if (variableHint && !hasElsif && this._isBlock(block)) { @@ -96,7 +96,7 @@ const ControlFlowHandlers = { const existingComment = this._context.comments[block.comment]; if (existingComment) existingComment.text += `,${varCommentText}`; } else { - const commentId = this._createComment(varCommentText, block.id, 0, 0, true); + const commentId = this._createComment(varCommentText, block.id); block.comment = commentId; } } @@ -166,7 +166,7 @@ const ControlFlowHandlers = { ); // Add comment to condition block - const condCommentId = this._createComment(commentText, condBlock.id, 0, 0, true); + const condCommentId = this._createComment(commentText, condBlock.id); condBlock.comment = condCommentId; const elseStatement = convertWhen(index + 1); @@ -185,7 +185,7 @@ const ControlFlowHandlers = { comment.minimized = true; } } else { - const blockCommentId = this._createComment(commentText, block.id, 0, 0, true); + const blockCommentId = this._createComment(commentText, block.id); block.comment = blockCommentId; } } @@ -235,7 +235,7 @@ const ControlFlowHandlers = { const existingComment = this._context.comments[block.comment]; if (existingComment) existingComment.text += `,${varCommentText}`; } else { - const commentId = this._createComment(varCommentText, block.id, 0, 0, true); + const commentId = this._createComment(varCommentText, block.id); block.comment = commentId; } } @@ -303,7 +303,7 @@ const ControlFlowHandlers = { comment.minimized = true; } } else { - const commentId = this._createComment(commentText2, block.id, 0, 0, true); + const commentId = this._createComment(commentText2, block.id); block.comment = commentId; } } @@ -352,7 +352,7 @@ const ControlFlowHandlers = { if (variableHint) { commentText += `,@ruby:variable:${variableHint}`; } - const commentId = this._createComment(commentText, block.id, 0, 0, true); + const commentId = this._createComment(commentText, block.id); block.comment = commentId; } diff --git a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/comment-handler.js b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/comment-handler.js index c34b9f1088b..d32f32cbf7d 100644 --- a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/comment-handler.js +++ b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/comment-handler.js @@ -234,21 +234,29 @@ const CommentHandler = { return; } - const inlineMarker = isInline ? '@ruby:comment_position:inline\n' : ''; + // Place the user text FIRST, with any `@ruby:` metadata appended on + // following lines. Blockly v12 renders the collapsed comment as a + // single-line bar showing the first line — putting user text first + // keeps that bar readable. The Ruby generator's scrub_() filters + // by full-line equality / `startsWith('@ruby:')`, so line order + // doesn't affect round-tripping. (See ruby-generator/scrub.js.) + const inlineMarker = isInline ? '@ruby:comment_position:inline' : ''; if (block.comment) { // Block already has a comment - merge const existingComment = this._context.comments[block.comment]; if (existingComment) { - // Put user text before metadata lines const lines = existingComment.text.split('\n'); const metadataLines = lines.filter(l => l.startsWith('@ruby:')); - const newText = `${inlineMarker}${userText}\n${metadataLines.join('\n')}`; - existingComment.text = newText; + if (isInline && !metadataLines.includes(inlineMarker)) { + metadataLines.unshift(inlineMarker); + } + const trailing = metadataLines.length > 0 ? `\n${metadataLines.join('\n')}` : ''; + existingComment.text = `${userText}${trailing}`; } } else { // Create new comment for this block - const commentText = isInline ? `${inlineMarker}${userText}` : userText; + const commentText = isInline ? `${userText}\n${inlineMarker}` : userText; block.comment = this._createComment(commentText, block.id); } } diff --git a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/looks.js b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/looks.js index dea682ea326..7db2baab593 100644 --- a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/looks.js +++ b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/looks.js @@ -110,7 +110,7 @@ const LooksConverter = { } else if (symbolVar) { converter._addTextInput(block, 'MESSAGE', symbolVar, 'Hello!'); block.comment = converter.createComment( - `@ruby:method:${methodName}`, block.id, 200, 0 + `@ruby:method:${methodName}`, block.id ); } else { converter._addTextInput( @@ -118,7 +118,7 @@ const LooksConverter = { converter._isNumber(arg) ? arg.toString() : arg, 'Hello!' ); block.comment = converter.createComment( - `@ruby:method:${methodName}`, block.id, 200, 0 + `@ruby:method:${methodName}`, block.id ); } converter._addNumberInput(block, 'SECS', 'math_number', 1, 1); @@ -169,7 +169,7 @@ const LooksConverter = { } } - block.comment = converter.createComment(commentText, block.id, 200, 0); + block.comment = converter.createComment(commentText, block.id); if (!firstBlock) { firstBlock = block; diff --git a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/operators-math.js b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/operators-math.js index ef9421bc6f3..9f83eb50c1b 100644 --- a/packages/scratch-gui/src/lib/ruby-to-blocks-converter/operators-math.js +++ b/packages/scratch-gui/src/lib/ruby-to-blocks-converter/operators-math.js @@ -54,6 +54,13 @@ const OperatorsMath = { if (variable && variable.dataType === 'string') return null; } + // Skip when receiver is itself a string-result block (operator_join): + // a chained `+` like `(a.to_s + " ") + b.to_s` is string concatenation, + // not arithmetic. Defer to the dedicated string `+` handler. + if (operator === '+' && converter._isBlock(receiver) && receiver.opcode === 'operator_join') { + return null; + } + // Boolean values are not valid operands for numeric arithmetic if (converter._isTrue(rh) || converter._isFalse(rh)) return null; diff --git a/packages/scratch-gui/src/lib/scratch-blocks-comment-icon-patch.js b/packages/scratch-gui/src/lib/scratch-blocks-comment-icon-patch.js index a746f49c178..2f09db97199 100644 --- a/packages/scratch-gui/src/lib/scratch-blocks-comment-icon-patch.js +++ b/packages/scratch-gui/src/lib/scratch-blocks-comment-icon-patch.js @@ -36,6 +36,69 @@ * `Smalruby: toXML modernization` block in comment.js for details. */ +/** + * Inject a global CSS rule that hides Blockly comment SVG nodes carrying + * the Smalruby metadata marker (`data-smalruby-meta="true"`). The rule is + * appended once to
so it covers comments created later (the patched + * icon below stamps the marker after fireCreateEvent / setText). + * + * Smalruby's converter attaches comments such as `@ruby:method:to_s` to + * blocks so the generator can round-trip them back to Ruby. In Blockly + * v11 these were tiny icons — invisible noise. Blockly v12 renders the + * collapsed state as a horizontal bar with the comment text, which clutters + * the workspace (e.g. an array literal of 10 elements produces 11 stacked + * bars). The comment data must remain on the block for round-tripping; + * only the visual is suppressed. + */ +const ensureMetaCommentHideStyle = function () { + if (typeof document === 'undefined') return; + if (document.getElementById('smalruby-hide-meta-comments')) return; + const style = document.createElement('style'); + style.id = 'smalruby-hide-meta-comments'; + style.textContent = 'g.blocklyComment[data-smalruby-meta="true"]{display:none!important;}'; + document.head.appendChild(style); +}; + +/** + * Stamp `data-smalruby-meta="true"` onto the bubble's SVG group when the + * comment text is Smalruby internal metadata (`@ruby:` prefix). The CSS + * rule installed by `ensureMetaCommentHideStyle` does the actual hiding. + * Safe to call multiple times — sets / clears the attribute based on + * the current text. + * @param {object} bubble - The Blockly comment bubble + * @param {string} text - The current comment text + */ +const isMetadataOnly = function (text) { + if (typeof text !== 'string' || text.length === 0) return false; + // A comment is metadata-only when every non-empty line starts with `@ruby:`. + // User comments may be merged with an inline marker (e.g. + // `@ruby:comment_position:inline\n