Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions packages/js/src/decorator/tinyMCE.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { markers, languageProcessing } from "yoastseo";
import { markers } from "yoastseo";
import { forEach } from "lodash-es";
import { languageProcessing } from "yoastseo";

var MARK_TAG = "yoastmark";

Expand Down Expand Up @@ -35,28 +36,11 @@ function markTinyMCE( editor, paper, marks ) {
let html = editor.getContent();
html = markers.removeMarks( html );

/*
* Get the information whether we want to mark a specific part of the HTML. If we do, `fieldsToMark` should return an array with that information.
* For example, [ "subehading" ] means that we want to apply the markings in subheadings only, and not the other parts.
* `selectedHTML` is an array of the HTML parts that we want to apply the marking to.
*/
const { fieldsToMark, selectedHTML } = languageProcessing.getFieldsToMark( marks, html );

// Generate marked HTML.
forEach( marks, function( mark ) {
/*
* Classic editor uses double quotes for HTML attribute values. However, Block editor uses single quotes for HTML tag attributes,
* and that's why in `yoastseo`, we use single quotes for the attribute values when we create the marked object. As a result,
* the replacement did not work, as the marks passed by `yoastseo` did not match anything in the original text.
* This step is replacing the single quotes in the marked object output by `yoastseo` with double quotes.
* This way, we make sure that the replacement can find a match between the original text of the marked object and the text in the page.
*/
mark._properties.marked = languageProcessing.replaceSingleQuotesInTags( mark._properties.marked );
mark._properties.original = languageProcessing.replaceSingleQuotesInTags( mark._properties.original );

// Check if we want to mark only specific part of the HTML.
if ( fieldsToMark.length > 0 ) {
// Apply the marking to the selected HTML parts.
selectedHTML.forEach( element => {
const markedElement = mark.applyWithReplace( element );
html = html.replace( element, markedElement );
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,48 +1,8 @@
import { deConstructAnchor, markWordsInSentences, reConstructAnchor } from "../../../../src/languageProcessing/helpers/word/markWordsInSentences";
import { markWordsInSentences } from "../../../../src/languageProcessing/helpers/word/markWordsInSentences";
import Mark from "../../../../src/values/Mark";
import matchWordCustomHelper from "../../../../src/languageProcessing/languages/ja/helpers/matchTextWithWord";

describe( "Adds Yoast marks to specific words in a sentence", function() {
it( "should add Yoast marks to all instances of specified words in a sentence, except when there is an anchor," +
" the marking should not be applied to the anchor tag attribute", function() {
expect( markWordsInSentences(
[ "picket", "tile" ],
[ "Introducing Palisades Ceramic Picket Tile — the latest trend in <a href=\"https://www.tileclub.com/collections/ceramic-tile\"" +
" target=\"_blank\" rel=\"noopener\">ceramic tile</a>!" ],
"en_EN"
) ).toEqual( [
new Mark( {
marked: "Introducing Palisades Ceramic <yoastmark class='yoast-text-mark'>Picket Tile</yoastmark> — the latest trend in " +
"<a href=\"https://www.tileclub.com/" +
"collections/ceramic-tile\" target=\"_blank\" rel=\"noopener\">ceramic " +
"<yoastmark class='yoast-text-mark'>tile</yoastmark></a>!",
original: "Introducing Palisades Ceramic Picket Tile — the latest trend in " +
"<a href=\"https://www.tileclub.com/collections/ceramic-tile\"" +
" target=\"_blank\" rel=\"noopener\">ceramic tile</a>!" } ),
]
);
} );
it( "should add Yoast marks to all instances of specified words in a sentence, except when there are multiple anchors," +
" the marking should not be applied to the anchor tag attribute", function() {
expect( markWordsInSentences(
[ "picket", "tile" ],
[ "Introducing Palisades Ceramic <a href=\"https://www.tileclub.com/ceramic-tile\">Picket Tile</a> — " +
"the latest trend in <a href=\"https://www.tileclub.com/collections/ceramic-tile\"" +
" target=\"_blank\" rel=\"noopener\">ceramic tile</a>!" ],
"en_EN"
) ).toEqual( [
new Mark( {
marked: "Introducing Palisades Ceramic <a href=\"https://www.tileclub.com/ceramic-tile\"><yoastmark class='yoast-text-mark'>" +
"Picket Tile</yoastmark></a> — the latest trend in " +
"<a href=\"https://www.tileclub.com/" +
"collections/ceramic-tile\" target=\"_blank\" rel=\"noopener\">ceramic " +
"<yoastmark class='yoast-text-mark'>tile</yoastmark></a>!",
original: "Introducing Palisades Ceramic <a href=\"https://www.tileclub.com/ceramic-tile\">Picket Tile</a> — " +
"the latest trend in <a href=\"https://www.tileclub.com/collections/ceramic-tile\"" +
" target=\"_blank\" rel=\"noopener\">ceramic tile</a>!" } ),
]
);
} );
it( "should add Yoast marks to all instances of specified words in a sentence", function() {
expect( markWordsInSentences(
[ "turtle", "hamster" ],
Expand Down Expand Up @@ -113,7 +73,7 @@ describe( "Adds Yoast marks to specific words in a sentence for languages with c
new Mark( {
marked: "<yoastmark class='yoast-text-mark'>小さい花の刺繍</yoastmark>しかし、それは在庫切れでしたマキシドレス。",
original: "小さい花の刺繍しかし、それは在庫切れでしたマキシドレス。" } ),
]
]
);
} );

Expand Down Expand Up @@ -142,60 +102,3 @@ describe( "Adds Yoast marks to specific words in a sentence for languages with c
} );
} );

describe( "test the deconstructAnchor and reconstructAnchor helper", () => {
it( "correctly deconstructs and reconstructs an anchor", () => {
const testAnchor = "<a href=\"https://yoast.com\">This is yoast.</a>";
const deconstructedAnchor = deConstructAnchor( testAnchor );

expect( deconstructedAnchor ).toEqual( {
openTag: "<a href=\"https://yoast.com\">",
content: "This is yoast.",
} );

const reconstructedAnchor = reConstructAnchor( deconstructedAnchor.openTag, deconstructedAnchor.content );
expect( reconstructedAnchor ).toEqual( testAnchor );
} );

it( "correctly deconstructs and reconstructs an anchor that contains html elements itself", () => {
const testAnchor = "<a href=\"https://yoast.com\">This <i>is</i> <b>yoast</b>.</a>";
const deconstructedAnchor = deConstructAnchor( testAnchor );

expect( deconstructedAnchor ).toEqual( {
openTag: "<a href=\"https://yoast.com\">",
content: "This <i>is</i> <b>yoast</b>.",
} );

const reconstructedAnchor = reConstructAnchor( deconstructedAnchor.openTag, deconstructedAnchor.content );
expect( reconstructedAnchor ).toEqual( testAnchor );
} );

it( "correctly deconstructs and reconstructs an anchor if does not contain content", () => {
// Unrealistic Scenario. But protects against the bug that is solved in this PR:
// https://github.com/Yoast/wordpress-seo/pull/19373
const testAnchor = "<a href=\"https://yoast.com\"></a>";
const deconstructedAnchor = deConstructAnchor( testAnchor );

expect( deconstructedAnchor ).toEqual( {
openTag: "<a href=\"https://yoast.com\">",
content: "",
} );

const reconstructedAnchor = reConstructAnchor( deconstructedAnchor.openTag, deconstructedAnchor.content );
expect( reconstructedAnchor ).toEqual( testAnchor );
} );

it( "correctly deconstructs and reconstructs an anchor if content contains a newline", () => {
// Unrealistic Scenario. But protects against the bug that is solved in this PR:
// https://github.com/Yoast/wordpress-seo/pull/19373
const testAnchor = "<a href=\"https://yoast.com\">This is a line.\nAnd this is a line.</a>";
const deconstructedAnchor = deConstructAnchor( testAnchor );

expect( deconstructedAnchor ).toEqual( {
openTag: "<a href=\"https://yoast.com\">",
content: "This is a line.\nAnd this is a line.",
} );

const reconstructedAnchor = reConstructAnchor( deconstructedAnchor.openTag, deconstructedAnchor.content );
expect( reconstructedAnchor ).toEqual( testAnchor );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getSubheadings } from "./getSubheadings";
* @param {array} marks The array of mark objects.
* @param {string} html The html of the page where we want to apply the marking to.
*
* @returns {{selectedHTML: *[], fieldsToMark: *}} The selected part of the html we want to apply the marking to.
* @returns {{selectedHTML: *[], fieldsToMark: *}} The selected part of the html we want to apply the marking tp.
*/
export function getFieldsToMark( marks, html ) {
const fieldsToMark = uniq( flatten( marks.map( mark => {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,109 +1,28 @@
import { escapeRegExp } from "lodash-es";
import addMark from "../../../markers/addMarkSingleWord";
import Mark from "../../../values/Mark";
import getAnchorsFromText from "../link/getAnchorsFromText";
import matchWords from "../match/matchTextWithArray";
import arrayToRegex from "../regex/createRegexFromArray";

// Regex to deconstruct an anchor into open tag, content and close tag.
// [^] matches any character, including newline
const anchorDeconstructionRegex = /(<a[\s]+[^>]+>)([^]*?)(<\/a>)/;

/**
* Deconstructs an anchor to the opening tag and the content. The content is the anchor text.
* We don't return the closing tag since the value would always be the same, i.e. </a>.
*
* @param {string} anchor An anchor of the shape <a ...>...</a>.
*
* @returns {object} An object containing the opening tag and the content.
*/
export const deConstructAnchor = function( anchor ) {
// The const array mirrors the anchorDeconstructionRegex, using a comma to access the first element without a name.
const [ , openTag, content ] = anchor.match( anchorDeconstructionRegex );
return {
openTag: openTag,
content: content,
};
};

/**
* Reconstructs an anchor from an openTag, the content, and the closing tag.
*
* @param {string} openTag The opening tag of the anchor. Must be of the shape <a ...>.
* @param {string} content The text of the anchor.
*
* @returns {string} An anchor.
*/
export const reConstructAnchor = function( openTag, content ) {
return `${openTag}${content}</a>`;
};


/**
* Gets the anchors and marks the anchors' text if the words are found in it.
*
* @param {string} sentence The sentence to retrieve the anchors from.
* @param {RegExp} wordsRegex The regex of the words.
*
* @returns {Object} The anchors and the marked anchors.
*/
const getMarkedAnchors = function( sentence, wordsRegex ) {
// Retrieve the anchors.
const anchors = getAnchorsFromText( sentence );
// For every anchor, apply the markings only to the anchor tag.
const markedAnchors = anchors.map( anchor => {
// Retrieve the open tag and the content/anchor text.
const { openTag, content } = deConstructAnchor( anchor );

// Apply the marking to the anchor text if there is a match.
const markedAnchorText = content.replace( wordsRegex, ( x ) => addMark( x ) );

// Create a new anchor tag with a (marked) anchor text.
return reConstructAnchor( openTag, markedAnchorText );
} );
return { anchors, markedAnchors };
};
import addMark from "../../../markers/addMarkSingleWord";
import Mark from "../../../values/Mark";
import { escapeRegExp } from "lodash-es";

/**
* Adds marks to a sentence and merges marks if those are only separated by a space
* (e.g., if highlighting words "ballet" and "shoes" in a sentence "I have a lot of ballet shoes and other paraphernalia."
* the marks will be put around "ballet shoes" together, not "`ballet` `shoes`".)
*
* @param {string} sentence The sentence to mark words in.
* @param {[string]} wordsFoundInSentence The words to mark in the sentence.
* @param {[string]} topicFoundInSentence The words to mark in the sentence.
* @param {function} matchWordCustomHelper The language-specific helper function to match word in text.
*
* @returns {string} The sentence with marks.
*/
export const collectMarkingsInSentence = function( sentence, wordsFoundInSentence, matchWordCustomHelper ) {
wordsFoundInSentence = wordsFoundInSentence.map( word => escapeRegExp( word ) );
export const collectMarkingsInSentence = function( sentence, topicFoundInSentence, matchWordCustomHelper ) {
topicFoundInSentence = topicFoundInSentence.map( word => escapeRegExp( word ) );
// If a language has a custom helper to match words, we disable the word boundary when creating the regex.
const wordsRegex = matchWordCustomHelper ? arrayToRegex( wordsFoundInSentence, true ) : arrayToRegex( wordsFoundInSentence );

// Retrieve the anchors and mark the anchors' text if the words are found in the anchors' text.
const { anchors, markedAnchors } = getMarkedAnchors( sentence, wordsRegex );

let markup = sentence.replace( wordsRegex, function( x ) {
const topicRegex = matchWordCustomHelper ? arrayToRegex( topicFoundInSentence, true ) : arrayToRegex( topicFoundInSentence );
const markup = sentence.replace( topicRegex, function( x ) {
return addMark( x );
} );

/**
* In 'markup', we apply the markings also inside the anchor's attribute if there is a match, on top of
* marking the anchor's text.
* The step below is to replace the incorrectly marked anchors with the marked anchors that we want:
* where the markings are only applied in the anchor's text.
*/
if ( anchors.length > 0 ) {
const markupAnchors = getAnchorsFromText( markup );
for ( let i = 0; i < markupAnchors.length; i++ ) {
markup = markup.replace( markupAnchors[ i ], markedAnchors[ i ] );
}
}

/*
* If two marks are separated by only a space, remove the closing tag of the first mark and the opening tag of the
* second mark so that the two marks can be combined into one.
*/
return ( markup.replace( new RegExp( "</yoastmark> <yoastmark class='yoast-text-mark'>", "ig" ), " " ) );
};

Expand All @@ -118,15 +37,16 @@ export const collectMarkingsInSentence = function( sentence, wordsFoundInSentenc
* @returns {[string]} The sentences with marks.
*/
export function markWordsInSentences( wordsToMark, sentences, locale, matchWordCustomHelper ) {
let wordsFoundInSentence = [];
let topicFoundInSentence = [];
let markings = [];

sentences.forEach( function( sentence ) {
wordsFoundInSentence = matchWords( sentence, wordsToMark, locale, matchWordCustomHelper ).matches;
if ( wordsFoundInSentence.length > 0 ) {
topicFoundInSentence = matchWords( sentence, wordsToMark, locale, matchWordCustomHelper ).matches;

if ( topicFoundInSentence.length > 0 ) {
markings = markings.concat( new Mark( {
original: sentence,
marked: collectMarkingsInSentence( sentence, wordsFoundInSentence, matchWordCustomHelper ),
marked: collectMarkingsInSentence( sentence, topicFoundInSentence, matchWordCustomHelper ),
} ) );
}
} );
Expand Down
2 changes: 0 additions & 2 deletions packages/yoastseo/src/languageProcessing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { stripFullTags as stripHTMLTags } from "./helpers/sanitize/stripHTMLTags
import sanitizeString from "./helpers/sanitize/sanitizeString";
import { unifyAllSpaces } from "./helpers/sanitize/unifyWhitespace";
import removePunctuation from "./helpers/sanitize/removePunctuation";
import replaceSingleQuotesInTags from "./helpers/html/replaceQuotes";
import countMetaDescriptionLength from "./helpers/word/countMetaDescriptionLength";
import getLanguage from "./helpers/language/getLanguage";
import getSentences from "./helpers/sentence/getSentences";
Expand Down Expand Up @@ -66,5 +65,4 @@ export {
getSentences,
getFieldsToMark,
unifyAllSpaces,
replaceSingleQuotesInTags,
};