Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spellchecking - how to give JS devs more control #166

Open
johanneswilm opened this issue Jun 23, 2017 · 22 comments
Open

spellchecking - how to give JS devs more control #166

johanneswilm opened this issue Jun 23, 2017 · 22 comments

Comments

@johanneswilm
Copy link
Contributor

johanneswilm commented Jun 23, 2017

We have previously discussed the possibility of letting JS have more direct access to the browser internal spell checker. We refrained from that due to security concerns because users may have added secret terms to their dictionaries.

Because JS editors do at least some of the DOM changes manually, the browser cannot know exactly which parts are still in need of spell checking and which parts have been spell checked already.

Also, the language of the spell checker is set by the browser and not the editor, and this does not always make sense. For example, a JS editor may know that the language of a specific text is French, but the browser still applies the user's standard dictionary English to the text and on a small mobile device it may be difficult even for the user to change the language used by the spell checker.

What is needed:

  1. Some way for JavaScript to either execute spell checking itself using the dictionary resources present in the browser (possibly without any user customizations), OR

  2. a way for JavaScript to communicate to the browser that a specific range of text either has been spell checked already OR that it is not in need of spell checking.

Additionally, it would be good if the JavaScript could tell the browser which spellchecker language to use on a specific range of text. Lower priority, but still nice to have, would be for the JavaScript to be able to tell the browser that certain terms are field-related terms and therefore not in need of spell checking.

Whatever we come up with should be extendable to also be able to cover grammar checkers some time in the future.

@Reinmar
Copy link

Reinmar commented Jun 27, 2017

I think that there are two cases here:

  • the editor wants to perform spell checking itself (perhaps, with some aid from the browser),
  • the editor wants to integrate well with the native spellchecker.

We've been avoiding the first scenario because it's really tricky. Taken today's limitations, the spell checking engine needs to be placed on the server side (Grammarly, WebSpellChecker) and then it needs to be integrated very well with the client side. The last part is especially problematic because one service needs to be integrated with various editors. It also requires custom UI as we're not able to control the native context menu well. So it's getting costly and messy. E.g. when I checked it in the past Grammarly was crashing Draft, Medium, ProseMirror (I think) and to some extent CKEditor 5 too. So, resolving this situation could be one thing. I guess that what everyone would like to see is a non-intrusive way to underline (with a squiggle) ranges of text. If that was possible without changing the DOM a lot of things would become much easier (e.g. custom IME implementations? ;))

The second scenario is what most of us choose – to integrate best with the native spell checker. For us (CKEditor 5) it means two things:

  • Handling changes done by the spellchecker (beforeinput, I'm looking at you).
  • Telling browser to ignore certain markup when tokenizing the text in a search for words. E.g. wo<b>rd</b> is two words for most browsers. Also, which is even more important in collaborative editing, wo<span></span>rd is also sometimes considered to be two words (and we need those spans to represent other users selections). It'd be great if we could mark those inline elements to be ignored by the browser's spell checker.

@Reinmar
Copy link

Reinmar commented Jun 28, 2017

Handling changes done by the spellchecker (beforeinput, I'm looking at you).

Talking about spell checking – I've just stumbled upon a case where Chrome's spell checker replaces <strong>buold</strong> with <b>bold</b>. Those are the things which we don't like as they are super problematic to control.

@gked
Copy link

gked commented Oct 3, 2018

@Reinmar @johanneswilm

so are you looking to be able to do something like document.getElementById("elementId").spellCheckOff(startPos, endPos);?

@johanneswilm johanneswilm changed the title spellching - how to give JS devs more control spellchecking - how to give JS devs more control Oct 4, 2018
@johanneswilm
Copy link
Contributor Author

We may also need a spellCheckOn function that behaves similarly.

@ianstormtaylor
Copy link

Just wanted to second @Reinmar's issue with spellcheck replacing <strong>word</strong> with <b>word</b>. We saw the same thing while working on Slate, not sure if it's been fixed so far. More generally, spellcheck will also completely remove some surrounding <span> tags last I checked (even if they have their own attributes) which is also very frustrating to have to work around. I'd think that spellcheck should only operate on the text node level, and not mess with the hierarchy.

The other big issue we have with spellcheck is that the little red squiggles re-render with a visible flashing if we update the DOM via React while typing. And the flashing is visible to the end user which is not a great experience.

@johanneswilm
Copy link
Contributor Author

@ianstormtaylor "I'd think that spellcheck should only operate on the text node level, and not mess with the hierarchy." We discussed this earlier, and it turns out it's problematic for some browsers as they want to look at partially styled words as one word. So for example "fissh" (<strong>fi</strong>ssh). Their spellchecker will then either make "fish" or "fish" out of that. It's worse with elements that have some kind of meaning for the editor that the browser doesn't understand ( for example: fi<span class="selection-of-collaborator-14">ssh</span>).

@Reinmar
Copy link

Reinmar commented Oct 4, 2018

@gked What would document.getElementById("elementId").spellCheckOff(startPos, endPos); do? Disabled the native spell checker on the given range? I don't think it would be helpful in any way.

As I wrote in #166 (comment), there are two scenarios that we need to handle:

  1. Implementing a custom spell checker. That's done by companies like Grammarly and WebSpellChecker. They use spellcheck=false to disable the native spell checker globally for the entire editing host and then render some squiggles by adding <span>s do the DOM.

    The problem here is that you cannot easily integrate your spell checking engine with all (or most) rich text editor at once. Most modern rich text editors have an architecture which does not allow any other JS app to modify that editor's DOM. Modifying their DOM directly either have no effect (the editor reverts that) or crashes that editor. This is a case with Grammarly which had to be disabled in Medium, CKEditor 5, Draft, etc.

    A solution to that would be if we were able to render squiggles or any other text decoration without actually touching the DOM. I call it "range decoration". I'm not sure how (and if) this can be implemented by browsers – i.e. how to make ranges controlled via CSS. Theoretically, this could work similarly to ::selection, like this:

    const rangeId = range.decorate();
    ::range(rangeId) { 
       color: red;
    }

    Or one would be able to pass CSS props directly to range.decorate():

    range.decorate( 'color: red' );

    Anyway, such a feature would be a great help in dealing with many kinds of editing features. We could also use it to display (in real-time collaboration scenarios) selections of other users, markers for their comments, etc. without changing the DOM so without affecting IME or the native spellchecker (the blinking mentioned by @ianstormtaylor). Such a feature would also enable much greater composability of JS libraries. Grammarly or any other company would be able to build a spell checking solution for every rich text editor on the market without investing in plugins built for each of them.

  2. The second scenario is where a text editor tries to integrate best with a native spell checker. Here, as I mentioned, we should be able to handle all changes done by the user with beforeinput. Let's say that's covered already.

    The other issue in this scenario is how the browser handles:

    • Some edge cases like woo<b>rd</b>, woo<span>rd</span> or woo<span></span>rd. Right now, many of them are not handled correctly – "woo" and "rd" are treated as two separate words.
    • What happens when the text editor updates the DOM. Right now, that makes the squiggles for misspelled words to blink. The CKEditor 5's rendering engine is designed to make absolute minimum DOM changes because we knew how fragile IME or spell checking are but I understand @ianstormtaylor's problems with blinking squiggles when React rerenders the DOM. OTOH, I think that there's little that we can spec here – it's mostly about how spell checking engines are implemented on the browsers side.

@johanneswilm
Copy link
Contributor Author

I don't think it would be helpful in any way.

I disagree on that. In combination with a cancelable beforeinput event, this could make for something useful. For example, in a collaborative editor, one may want to disable spell checking for ranges of text that a collaborator already has checked and accepted. Or one may want to mark a range as dirty because it has not been checked before.

Implementing a custom spell checker. That's done by companies like Grammarly and WebSpellChecker.

For Fidus Writer I implemented it using the open source LanguageTool which is simply bundling most of the open source dictionaries/grammar checkers there are out there. The size of that package is around 160MB for a set of mot major languages. Maybe in 10 years time that is something we can just embed in a webpage, but for now it still needs to be done server side. But the integration of that has to take place through a specialized connector in the JS editor that is aware of what is what -- for example, in the implementation I needed to make sure it ignores citations as well as text that has been tracked as deleted. If one wanted to create something that works on all JS editors without specialized plugins, one would have to have each of the JS editors implement some kind of serialization/deserialization method that ignores/hides content that should not be taken into consideration when doing the check.

  • Some edge cases like woo<b>rd</b>, woo<span>rd</span> or woo<span></span>rd. Right now, many of them are not handled correctly – "woo" and "rd" are treated as two separate words.

That does not look good. Have you tried this on all browsers on all OSes? Last I checked on mobile, Android would let words go across such boundaries.

@johanneswilm
Copy link
Contributor Author

An example of how complex it can get with a grammar checker: Citations in FW can basically be either shown as "(Einstein, 1932: p.34)" or "Einstein (1932: p.34)". The second case is used if it is used as subject/object of a sentence, such as "Even Einstein (1932: p. 34) thought this was a huge problem." But the "Einstein" needs to be part of the "citation" object, because depending on the citation style, it may be rendered as "Einstein", "A. Einstein", "Albert E.", etc. . So now the grammar checker sees this, and knowing nothing about the JS editor and how it renders things, It just sees "Even [CITATION] thought this was a huge problem." and it will complain about a missing subject in that sentence.

@Reinmar
Copy link

Reinmar commented Oct 4, 2018

I disagree on that. In combination with a cancelable beforeinput event, this could make for something useful. For example, in a collaborative editor, one may want to disable spell checking for ranges of text that a collaborator already has checked and accepted. Or one may want to mark a range as dirty because it has not been checked before.

👍 I haven't thought about such scenarios but that makes a lot of sense. So yes, 👍 for such a method.

The size of that package is around 160MB for a set of mot major languages. Maybe in 10 years time that is something we can just embed in a webpage, but for now it still needs to be done server side.

Totally agree. Today (and I think that won't change) this should be done on the server side. But that itself doesn't change the picture of what happens on the client. It just happens slower.

But the integration of that has to take place through a specialized connector in the JS editor that is aware of what is what -- for example, in the implementation I needed to make sure it ignores citations as well as text that has been tracked as deleted.

I agree that in more specialised use cases the spell checker must be tightly integrated with the text editor itself. But:

  • In simpler scenarios (and that's a really big percentage) the entire text can be spell checked because that text contains no additional meta data that needs to be skipped. The popularity of Grammarly (which I use right now) shows that this is something worth considering.
  • In more advanced cases we may not be able to do much without tight integrations. However, with contentEditable=false/true and spellcheck=false/true you can guide those external, generic spell checkers where they should be on and where off. It actually works this way now with both – the native spell checkers and Grammarly – they don't check the text in cE=false containers and they re-enable themselves for nested cE=true islands.

@johanneswilm
Copy link
Contributor Author

In simpler scenarios (and that's a really big percentage)

The problem is that if it's 99% simple text but there is just one slightly more complex element that gets messed up on just one of the browser/OS combinations, that already makes it quite unusabffdle. Or if the end user with her/his Grammarly subscription is able to edit social media comments on most platforms correctly, but that one day he/she needs to hand in a school essay it messes up the entire paper 5 minutes before the deadline, it's highly annoying.

I just tried Grammarly again for the first time in a few years. It seems to work in this issue, but in Gmail it just managed to delete an entire email I had typed already. That's not really helpful. What it does is it creates an element that overlays the existing textarea/CE with the same content but with marked ranges and then it hopes that this doesn't interfere with the JS app it's working on. This works in Github comments, but apparently doesn't always work in Gmail.

However, server-based spell checking doesn't always make sense. Some pages may need to work while offline. And it doesn't really make sense to spend a lot of server resources on something that the browser can do already given that browsers OSes come bundled with spellcheckers.

So a combination of these things should be helpful in all cases:

  • A way to mark ranges of text without having to add new elements. This should give Grammarly & co alternatives for adding their mark and prevent them from destroying web pages.

  • A way to mark certain ranges of text as being in need of spell checking or having been spell checked already, without having to add new elements.

  • Ideally: a way to access the OS/browser-based spellchecker. For security reasons this may be the dictionary as it was shipped with the browser/OS without custom entries made by the user.

@ianstormtaylor
Copy link

We discussed this earlier, and it turns out it's problematic for some browsers as they want to look at partially styled words as one word. So for example "fissh" (fissh). Their spellchecker will then either make "fish" or "fish" out of that. It's worse with elements that have some kind of meaning for the editor that the browser doesn't understand ( for example: fissh).

I still think if you constrain the problem to only operation on text nodes you end up with a better solution here that editors are more likely to be able to work with. For example, given this starting point:

<strong>fi</strong>ssh

If the browser decides it really needs to combine the letters under a single element, it can perform what amounts to a "remove" to erase ssh and an "insert" to insert sh into the <strong> element, ending with:

<strong>fish</strong>

(But it still hasn't modified or created any non text nodes.)

But I think that's still not great. I think browsers should strive to not mess with which letters appear nested in which elements either even. Instead, in that case they should just do a diff to figure out they need to remove the extra s, and end up with:

<strong>fi</strong>sh

This would leave the DOM in the most initial-like state, and would be what editors expect a spellcheck to do.

@ianstormtaylor
Copy link

What it does is it creates an element that overlays the existing textarea/CE with the same content but with marked ranges and then it hopes that this doesn't interfere with the JS app it's working on.

FWIW too, the range.decorate concept could also be useful for displaying multiple cursors, as I believe the "duplicate content overlay" is how many editors end up implementing those as well, which seems like a huge pain to implement and maintain.

@johanneswilm
Copy link
Contributor Author

I still think if you constrain the problem to only operation on text nodes you end up with a better solution here that editors are more likely to be able to work with.

I think most (all?) of us on the JS side of things tend to think that way, but at least last we checked, browser people thought differently about this and felt like they needed to check "partially styled words" like that. That we came up, as a working compromise, to have a cancelable before input event and a way to enable/disable spell checking for specific ranges and/or to have a way to communicate to the browser that certain element boundaries are to be regarded as word boundaries. For example in <p>Hel<strong>lo</strong> Pet<img src="...">er!</p>" the first strong should maybe not be regarded as a word boundary, whereas the ` could or could not be a word boundary depending a bit on how large the image is.

Things may have changed now. It's interesting to hear @Reinmar say that they do only check within text nodes. I wonder if that also is the case in IMEs on mobile, for example.

If the browser decides it really needs to combine the letters under a single element, it can perform what amounts to a "remove" to erase ssh and an "insert" to insert sh into the <strong> element.

I think that's not really the best solution, as it doesn't tell the JS what the semantic purpose of those operations is. Maybe the JS decides it wants to ignore the spell checking entirely. Or it wants to show an animation in the case of spell checking fixes. Yet those operations don't really tell it what the user asked for.

Instead, it should create an atomic operation with a beforeinput event with the inputType set to "insertReplacementText". The target range should cover the entire range that the spell checking covers. That way the JS can cancel this beforeinput event and handle it exactly as it intended to do itself, figuring out how to deal with the strong element in there.

@ianstormtaylor
Copy link

@johanneswilm that makes sense about word boundaries. But I think the difference for me is that there's a distinction between how the spellcheck recognizes words, and how it treats existing markup when "fixing" the words. In my mind, there's nothing stopping them from recognizing words across inline elements, but still "fixing" the errors without changing the structure of those inline elements.

Instead, it should create an atomic operation with a beforeinput event with the inputType set to "insertReplacementText". The target range should cover the entire range that the spell checking covers. That way the JS can cancel this beforeinput event and handle it exactly as it intended to do itself, figuring out how to deal with the strong element in there.

Right, right. I'm not talking about the actual events that fire. What I'm trying to point out is that it's ridiculous that browsers are editing the actual markup, instead of just changing the text nodes in response to these beforeinput events.

I don't understand why:

<strong>fi</strong>ssh

Would ever turn into:

<b>fish</b>

Or even:

<strong>fish</strong>

When it could just as easily be kept intact as:

<strong>fi</strong>sh

While still applying the "fix" for the spelling.

I'm not arguing against the existing spec for beforeinput at all, I think those are great. It's just the actual DOM mutations that browsers are performing for their native handling of these beforeinput events that makes no sense, and makes it a huge pain in the ass for JavaScript editors to deal with the aftermaths.

@johanneswilm
Copy link
Contributor Author

I don't understand why [...]

I think the explanation here is that they serialize it all into plaintext before applying the spell checking. And then when applying the spell checking then don't really have an easy and effortless way of reapplying the styling.

I think a lot of this is about cultural differences. And I'm not talking culture differences between countries, religions or socio-economic differences. Browser devs seem to have a different experience of how it's all used than JS devs who are into this area. The browser devs seem to mostly have in mind cases where very little or no JavaScript is being used and where elements use the semantic meaning they have and where browsers can by default do DOM modifications that are specific to the platform they are on.

Say on an iPhone, the browser may move the entire word into the <strong>, whereas on a PC it deletes the <strong> because that is how other, native richtext editors behave on those platforms and they want to preserve that behavior when it comes to the web. All the JS devs working in this area that I have spoken with so far, on the other hand, almost don't seem to care about any platform specific behavior and are more concerned about being able to create JS editors that behave the same (and work at all) on all platform combinations.

So the compromise we came up with was kind of that the browsers do whatever DOM modifications they need to do, but that those can be replaced by defining alternative behavior using the beforeinput events. Unfortunately Chrome ended up implementing the beforeinput event in a way where most things are not cancelable, so to make it cancelable anyway, one needs to combine with a DOM diffing mechanism that can then roll back the native browser changes before reapplying them... And yes, I know what a pain that is.

But opinions change and we can try to debate this again.

And yes, the <strong> => <b> doesn't seem to make sense to me either. About 3-4 years ago, it was still creating a bunch of <font>-elements though. So it's an improvement.

@gked
Copy link

gked commented Oct 5, 2018

Edge and Firefox do not replace <strong> the <b> tags so this definitely sounds like a bug on Chrome. Looks like there is already a bug on Chrome about this.

@jimmywarting
Copy link

I stumble upon a visual markdown editor today that render everything to actual DOM elements, the tradeoff from not using a regular textarea was the lost of spellchecking 😞 there was a solution doe and it was do embed a large dictionary which i didn't want to do.
It would be cool if i could expand the spellcheck attribute on other elements too like a <p> element

@erickponce
Copy link

Please we need a way to check if an element has misspelled words with JavaScript 😞
Like a new onSpellError event.

@gked
Copy link

gked commented Aug 14, 2020

@erickponce could you please expand on scenarios you are looking to accomplish with onSpellError?

@erickponce
Copy link

Hello there @gked 😀

Thank's for the reply, basically the company I work has a chat app with a spellcheck feature that need's to block the user ability to send the message if there's a misspelled word.

We had to implement a whole spellcheck system using backend and a lot of frontend hacks to be able to do that.
If we had a way to check if the element has misspelled words, I think 3 simple lines of code would have solved the issue.

On a side note, I personally don't like the idea of blocking the user ability to send the message, but... that's a whole other story right 😅?

@Naheulf
Copy link

Naheulf commented Aug 30, 2020

The size of that package is around 160MB for a set of mot major languages. Maybe in 10 years time that is something we can just embed in a webpage, but for now it still needs to be done server side.

Totally agree. Today (and I think that won't change) this should be done on the server side. But that itself doesn't change the picture of what happens on the client. It just happens slower.

I think there two uses cases:

  • when the editor (like @erickponce's company) want to include some check in the web page. In that case I partially disagree with you because the editor can know the language used. So only one or two languages are downloaded, And only one time if the browser web cache is used.
  • when someone (a company or a free project) want to create a browser extension. In that case I totally disagree with you because all dictionaries and check rules can be stored on client side and the extension can load them when it's needed.

However, server-based spell checking doesn't always make sense. Some pages may need to work while offline. And it doesn't really make sense to spend a lot of server resources on something that the browser can do already given that browsers OSes come bundled with spellcheckers.

I agree with you but I want to include another difference:

  • Level 1: spell check: check if some worrd word exist in a dictionnary dictionary.
  • Level 2: grammar/syntax check: check if all word words creates create a sentence without any error errors.
  • Level 3: semantics check: check if there no bugs errors in the sentence.

Notes:

  • Browsers perform only spell check, few addons/web-service perform grammar check but no-one perform semantics check.
  • Each checker may also include few statics "find and replace" rules to perform few checks on very common errors from above levels.
  • Each checker also check for below levels.

For info there a French open-source grammar checker (grammalecte) that perform Both spell AND grammar check on client side. The grammar checker runs in a Worker launched from the background script. (But only check french)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants