Preserve trailing space in last paragraph segment by converting to nbsp (#3235) (#3287)
- Preserve trailing space in last paragraph segment by converting to nbsp
When the last text segment in a paragraph ends with a regular space, browsers collapse it during rendering. This change detects that case in handleText and replaces the trailing space with a non-breaking space (\u00A0) so it is preserved in the output.
To support this, a new ModelToDomSegmentContext interface is introduced that extends ModelToDomContext with an isLastSegment flag. handleParagraph sets this flag for each segment before dispatching, and ContentModelSegmentHandler is updated to use ModelToDomSegmentContext as its context type, eliminating the need for type casts in the handlers.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
- Apply suggestions from code review
Co-authored-by: Copilot 175728472+Copilot@users.noreply.github.com
- Improve trailing space to nbsp conversion with noFollowingTextSegmentOrLast
Refactor handleParagraph to track whether a text segment is the last in the paragraph or has no following text segment (excluding SelectionMarkers). This ensures trailing spaces are converted to not only for the very last segment, but also when the next non-marker segment is not a Text segment.
- Convert forEach to for loop in handleParagraph for segment iteration
- Extract hasTextSegmentAfter helper to check for upcoming text segments
- Add noFollowingTextSegmentOrLast property to ModelToDomSegmentContext
- Update handleText to use the new property name
- Fix stale isLastSegment references in handleTextTest
- Add comprehensive tests for noFollowingTextSegmentOrLast in handleParagraphTest
Co-authored-by: Claude Sonnet 4.6 noreply@anthropic.com Co-authored-by: Copilot 175728472+Copilot@users.noreply.github.com
版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9
京公网安备 11010802032778号
Rooster
Rooster is a framework-independent JavaScript rich-text editor neatly nested inside one HTML
<div>element. Editing operations performed by end users are handled in simple ways to generate the final HTML.Rooster is working on top of a middle layer data structure called “Content Model”. All format API and editing operation are using this Content Model layer as content format, and finally convert to HTML and show it in editor.
To view the demo site, please click the link below:
RoosterJs Demo Site.
Upgrade from RoosterJs 8.*
Please see here.
Features
Packages
Rooster contains 6 basic packages.
roosterjs: A facade of all Rooster code for those who want a quick start. Use the
createEditor()function in roosterjs to create an editor with default configurations.roosterjs-content-model-core: Defines the core editor and plugin infrastructure. Use
roosterjs-content-model-coreinstead ofroosterjsto build and customize your own editor.roosterjs-content-model-api: Defines APIs for editor operations. Use these APIs to modify content and formatting in the editor you built using
roosterjs-content-model-core.roosterjs-content-model-dom: Defines APIs for Content Model and DOM operations. This package do conversion between DOM tree and roosterjs Content Model.
roosterjs-content-model-plugins: Defines basic plugins for common features.
roosterjs-content-model-types: Defines public interfaces and enumerations, including Content Model types, API parameters and other types.
There are also some extension packages to provide additional functionalities.
roosterjs-color-utils: Provide color transformation utility to make editor work under dark mode.
roosterjs-content-model-markdown: Defines public APIs to enable conversions between Markdown and ContentModel
To be compatible with old (8.*) versions, you can use
EditorAdapterclass from the following package which can act as a 8.* Editor:EditorAdapterto work with Editor (9.*) and legacy plugins (via EditorAdapterOptions.legacyPlugins)All old packages (8.*) are moved to branch roosterjsv8, including
We will not update these branches any more unless there are new security bugs.
APIs
Rooster provides Content Model level APIs (in
roosterjs-content-model-dom), core APIs (inroosterjs-content-model-core), and formatting APIs (inroosterjs-content-modelapi) to perform editing operations.roosterjs-content-model-domprovides several levels of Content Model operations:roosterjs-content-model-coreprovides APIs for editor core. Editor class will call such APIs to perform basic editor operations. These APIs can be overridden by specifying API overrides in Editor options when creating the editor.roosterjs-content-model-apiprovides APIs for scenario-based operations triggered by user interaction.roosterjs-content-model-markdownprovides API to transform Markdown language in Content Model objects.Plugins
Rooster supports plugins. You can use built-in plugins or build your own. Plugins call APIs to communicate with the editor. When an operation is performed by the user or when content is changed by code, the editor will trigger events for the plugins to handle.
Here’s a sample plugin which will show a dialog containing “Hello Rooster” when an “a” is typed in the editor:
Installation
Install via NPM or Yarn:
yarn add roosterjsYou can also install sub packages separately:
yarn add roosterjs-content-model-coreyarn add roosterjs-content-model-api...In order to run the code below, you may also need to install webpack:
yarn add webpack -gUsage
A quick start
editor.htmwhich contains a DIV with some styles, buttons to handle some click events and a reference to rooster.js (update with the path to your rooster.js file):Sample code
To view the demo site, please click here.
To build the demo site code yourself, follow these instructions:
Get dependencies using yarn or npm:
Build the source code, and start the sample editor:
or
Debugging
There are two options for debugging:
Debugging from VSCode
Debugging directly from the development tools within the web browser
Running tests
There are two ways that tests can be run:
Dependencies
As a NodeJs package, RoosterJs has dependencies for runtime (specified in package.json under each sub packages in “dependencies” section) and dependencies for build time (specified in package.json under root path in “devDependencies” section).
For runtime dependencies, there are two parts:
Currently we have very few external dependencies. Before adding any new dependency, we need to check:
What’s the value of the new dependency and the code using the dependency bring into roosterjs? If we add a new dependency and create our new API to just call into the dependency, that new API doesn’t actually bring too much value, and people who uses roosterjs in their project can do this themselves in their code, and we should not add such dependency to people who don’t really need it.
What’s the dependency tree of the dependency? If we introduce a new dependency which has a deep dependency tree, we need to be careful since it means we are actually adding a lot of new dependencies and our code size may be increased a lot.
How much functionalities do we need from the dependency? If the dependency provides a lot of functionalities but we actually only need a small piece of them, we may need to consider other solutions, such as find another smaller one, or do it ourselves.
What’s the license of the dependency? A dependency package under MIT license is good to be used for RoosterJs. For other licenses, we need to review and see if we can take it as a dependency.
If you still feel a new dependency is required after checking these questions, we can review it and finally decide whether we should add the new dependency.
For build time dependencies, it is more flexible to add new dependencies since it won’t increase runtime code size or dependencies.
More documentation
We are still working on more documentation in roosterjs wiki and API reference.
License - MIT
License Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.