diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs
index bed3c50e2d..a8efb4d02e 100644
--- a/ghost/admin/app/components/gh-koenig-editor-lexical.hbs
+++ b/ghost/admin/app/components/gh-koenig-editor-lexical.hbs
@@ -28,6 +28,15 @@
{{/if}}
+ {{#if (and (feature "tkReminders") @titleHasTk)}}
+
+ TK
+
+ {{/if}}
+
diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js
index 5f6238ef6a..3935a8d99b 100644
--- a/ghost/admin/app/components/koenig-lexical-editor.js
+++ b/ghost/admin/app/components/koenig-lexical-editor.js
@@ -536,7 +536,7 @@ export default class KoenigLexicalEditor extends Component {
registerAPI={this.args.registerAPI}
/>
- {this.feature.tkReminders && }
+ {this.feature.tkReminders && }
diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js
index a85e31b9c6..ce21935936 100644
--- a/ghost/admin/app/controllers/lexical-editor.js
+++ b/ghost/admin/app/controllers/lexical-editor.js
@@ -35,6 +35,9 @@ const AUTOSAVE_TIMEOUT = 3000;
// time in ms to force a save if the user is continuously typing
const TIMEDSAVE_TIMEOUT = 60000;
+const TK_REGEX = new RegExp(/(^|.)([^\p{L}\p{N}\s]*(TK)+[^\p{L}\p{N}\s]*)(.)?/u);
+const WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u);
+
// this array will hold properties we need to watch for this.hasDirtyAttributes
let watchedProps = [
'post.lexicalScratch',
@@ -127,7 +130,7 @@ export default class LexicalEditorController extends Controller {
// koenig related properties
wordCount = 0;
- tkCount = 0;
+ postTkCount = 0;
/* private properties ----------------------------------------------------*/
@@ -215,6 +218,55 @@ export default class LexicalEditorController extends Controller {
return config.environment !== 'test' && this.get('post.isDraft');
}
+ TK_REGEX = new RegExp(/(^|.)([^\p{L}\p{N}\s]*(TK)+[^\p{L}\p{N}\s]*)(.)?/u);
+ WORD_CHAR_REGEX = new RegExp(/\p{L}|\p{N}/u);
+
+ @computed('post.titleScratch')
+ get titleHasTk() {
+ let text = this.post.titleScratch;
+ let matchArr = TK_REGEX.exec(text);
+
+ if (matchArr === null) {
+ return false;
+ }
+
+ function isValidMatch(match) {
+ // negative lookbehind isn't supported before Safari 16.4
+ // so we capture the preceding char and test it here
+ if (match[1] && match[1].trim() && WORD_CHAR_REGEX.test(match[1])) {
+ return false;
+ }
+
+ // we also check any following char in code to avoid an overly
+ // complex regex when looking for word-chars following the optional
+ // trailing symbol char
+ if (match[4] && match[4].trim() && WORD_CHAR_REGEX.test(match[4])) {
+ return false;
+ }
+
+ return true;
+ }
+
+ // our regex will match invalid TKs because we can't use negative lookbehind
+ // so we need to loop through the matches discarding any that are invalid
+ // and moving on to any subsequent matches
+ while (matchArr !== null && !isValidMatch(matchArr)) {
+ text = text.slice(matchArr.index + matchArr[0].length - 1);
+ matchArr = TK_REGEX.exec(text);
+ }
+
+ if (matchArr === null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @computed('titleHasTk', 'postTkCount')
+ get tkCount() {
+ return (this.titleHasTk ? 1 : 0) + this.postTkCount;
+ }
+
@action
updateScratch(lexical) {
this.set('post.lexicalScratch', JSON.stringify(lexical));
@@ -312,8 +364,8 @@ export default class LexicalEditorController extends Controller {
}
@action
- updateTkCount(count) {
- this.set('tkCount', count);
+ updatePostTkCount(count) {
+ this.set('postTkCount', count);
}
@action
@@ -1082,7 +1134,7 @@ export default class LexicalEditorController extends Controller {
this.set('shouldFocusTitle', false);
this.set('showSettingsMenu', false);
this.set('wordCount', 0);
- this.set('tkCount', 0);
+ this.set('postTkCount', 0);
// remove the onbeforeunload handler as it's only relevant whilst on
// the editor route
diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css
index 6c944f07ee..2af885244b 100644
--- a/ghost/admin/app/styles/layouts/editor.css
+++ b/ghost/admin/app/styles/layouts/editor.css
@@ -683,6 +683,17 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
opacity: .5;
}
+.gh-editor-title-container .tk-indicator {
+ position: absolute;
+ top: 15px;
+ right: -5.6rem;
+ padding: .4rem;
+ color: #95A1AD;
+ font-size: 1.2rem;
+ font-weight: 500;
+ cursor: default;
+}
+
.gh-editor-back-button {
height: 34px;
margin-right: 8px;
diff --git a/ghost/admin/app/templates/lexical-editor.hbs b/ghost/admin/app/templates/lexical-editor.hbs
index 3639085ef5..d0cb2d14b2 100644
--- a/ghost/admin/app/templates/lexical-editor.hbs
+++ b/ghost/admin/app/templates/lexical-editor.hbs
@@ -67,6 +67,7 @@
@title={{readonly this.post.titleScratch}}
@titleAutofocus={{this.shouldFocusTitle}}
@titlePlaceholder={{concat (capitalize this.post.displayName) " title"}}
+ @titleHasTk={{this.titleHasTk}}
@onTitleChange={{this.updateTitleScratch}}
@onTitleBlur={{perform this.saveTitleTask}}
@body={{readonly this.post.lexicalScratch}}
@@ -77,7 +78,7 @@
@scrollOffsetBottomSelector=".gh-mobile-nav-bar"
@onEditorCreated={{this.setKoenigEditor}}
@updateWordCount={{this.updateWordCount}}
- @updateTkCount={{this.updateTkCount}}
+ @updatePostTkCount={{this.updatePostTkCount}}
@featureImage={{this.post.featureImage}}
@featureImageAlt={{this.post.featureImageAlt}}
@featureImageCaption={{this.post.featureImageCaption}}