3

I have a file strings.json which contains

{
  "fname": "First Name",
  "lname": "Last Name",
  "email": "Email"
}

I have another file index.html which contains

  First name <input type="text" name="fname">
  Last name 
  Email 

If the html file contains a string that exists in the json file, I'd like to wrap it with a span and data-attribute, using Javascript or JQuery, for example:

 <span data-string="fname">First name</span><input type="text" name="fname">
 <span data-string="lname">Last name</span>
 <span data-string="email">Email</span>

I don't want solutions like this question, my question is a lot similar but the key difference is that I want an agnostic code, that will work without knowing the structure of the html,and I don't want regex that is specific for one structure either.

You just give it string in body, it wraps all the results in a span like shown above and that's it. I haven't found an agnostic way yet. Another example is this question, not agnostic either, you have to know the html structure for it to work.

Markjs didn't work for 2 reasons:

  • I want to use it to select strings, it's highlighting them and I don't know how to turn highlight off
  • More importantly :

    var myString = $("body").mark("Hi");
    console.log(myString);
    // returns
    
    translate.html:17 r.fn.init(1)0: bodyaLink: ""accessKey: ""assignedSlot: nullattributes: NamedNodeMap {length: 0}background: ""baseURI: "file:///home/elie/Desktop/csc/translate.html"bgColor: ""childElementCount: 7childNodes: (14) [text, h1, br, text, mark, text, script, text, script, text, script, text, script, text]children: (7) [h1, br, mark, script, script, script, script]classList: [value: ""]className: ""clientHeight: 88clientLeft: 0clientTop: 0clientWidth: 1264contentEditable: "inherit"dataset: DOMStringMap {}dir: ""draggable: falsefirstChild: textfirstElementChild: h1hidden: falseid: ""innerHTML: "↵   <h1><mark data-markjs="true">Hi</mark>,</h1><br>↵   T<mark data-markjs="true">hi</mark>s is the text that will be translated↵↵↵<script src="jquery-3.2.1.min.js"></script>↵<script src="jquery.mark.min.js"></script>↵<script src="jquery.localize.min.js"></script>↵↵<script type="text/javascript">↵var myString = $("body").mark("Hi");↵console.log(myString);↵</script>↵↵"innerText: "Hi,↵↵↵This is the text that will be translated"isConnected: trueisContentEditable: falselang: ""lastChild: textlastElementChild: scriptlink: ""localName: "body"namespaceURI: "http://www.w3.org/1999/xhtml"nextElementSibling: nullnextSibling: nullnodeName: "BODY"nodeType: 1nodeValue: nullnonce: ""offsetHeight: 88offsetLeft: 0offsetParent: nulloffsetTop: 0offsetWidth: 1264onabort: nullonauxclick: nullonbeforecopy: nullonbeforecut: nullonbeforepaste: nullonbeforeunload: nullonblur: nulloncancel: nulloncanplay: nulloncanplaythrough: nullonchange: nullonclick: nullonclose: nulloncontextmenu: nulloncopy: nulloncuechange: nulloncut: nullondblclick: nullondrag: nullondragend: nullondragenter: nullondragleave: nullondragover: nullondragstart: nullondrop: nullondurationchange: nullonemptied: nullonended: nullonerror: nullonfocus: nullongotpointercapture: nullonhashchange: nulloninput: nulloninvalid: nullonkeydown: nullonkeypress: nullonkeyup: nullonlanguagechange: nullonload: nullonloadeddata: nullonloadedmetadata: nullonloadstart: nullonlostpointercapture: nullonmessage: nullonmessageerror: nullonmousedown: nullonmouseenter: nullonmouseleave: nullonmousemove: nullonmouseout: nullonmouseover: nullonmouseup: nullonmousewheel: nullonoffline: nullononline: nullonpagehide: nullonpageshow: nullonpaste: nullonpause: nullonplay: nullonplaying: nullonpointercancel: nullonpointerdown: nullonpointerenter: nullonpointerleave: nullonpointermove: nullonpointerout: nullonpointerover: nullonpointerup: nullonpopstate: nullonprogress: nullonratechange: nullonrejectionhandled: nullonreset: nullonresize: nullonscroll: nullonsearch: nullonseeked: nullonseeking: nullonselect: nullonselectstart: nullonstalled: nullonstorage: nullonsubmit: nullonsuspend: nullontimeupdate: nullontoggle: nullonunhandledrejection: nullonunload: nullonvolumechange: nullonwaiting: nullonwebkitfullscreenchange: nullonwebkitfullscreenerror: nullonwheel: nullouterHTML: "<body>↵ <h1><mark data-markjs="true">Hi</mark>,</h1><br>↵   T<mark data-markjs="true">hi</mark>s is the text that will be translated↵↵↵<script src="jquery-3.2.1.min.js"></script>↵<script src="jquery.mark.min.js"></script>↵<script src="jquery.localize.min.js"></script>↵↵<script type="text/javascript">↵var myString = $("body").mark("Hi");↵console.log(myString);↵</script>↵↵</body>"outerText: "Hi,↵↵↵This is the text that will be translated"ownerDocument: documentparentElement: htmlparentNode: htmlprefix: nullpreviousElementSibling: headpreviousSibling: textscrollHeight: 88scrollLeft: 0scrollTop: 0scrollWidth: 1264shadowRoot: nullslot: ""spellcheck: truestyle: CSSStyleDeclaration {alignContent: "", alignItems: "", alignSelf: "", alignmentBaseline: "", all: "", …}tabIndex: -1tagName: "BODY"text: ""textContent: "↵  Hi,↵    This is the text that will be translated↵↵↵↵↵↵↵↵var myString = $("body").mark("Hi");↵console.log(myString);↵↵↵"title: ""translate: truevLink: ""__proto__: HTMLBodyElementlength: 1prevObject: [document]__proto__: Object(0)
    

I want console.log to return Hi

Lynob
  • 5,059
  • 15
  • 64
  • 114
  • 2
    There are highlighter plugins around that will do this for you. Why re-invent the wheel? Question is far too broad and really expects others to do all the code work. There are numerous "gotchas" in doing htis – charlietfl Nov 27 '17 at 20:35
  • @charlietfl I thought so but haven't found any, could you tell me about one that doesn't have dependencies other than jquery? in fact I prefer plugins – Lynob Nov 27 '17 at 20:37
  • Check out https://markjs.io/. – ibrahim mahrir Nov 27 '17 at 20:39
  • @ibrahimmahrir could you post an answer so i could accept and close this question? – Lynob Nov 28 '17 at 19:10
  • @ibrahimmahrir markjs didn't work unless you know a workaround – Lynob Nov 29 '17 at 13:37
  • @Lynob Sorry to hear. That the best I can think of. I haven't used markjs a lot of time so I'm not an expert at it thus I don't know any workarounds. Sorry. – ibrahim mahrir Nov 29 '17 at 15:03
  • What problem are you trying to solve? If you are looking for highlighting there are plenty of plugins for that as others have said. Is it supposed to be case insensitive? In your examples the `Name` and `name` don't match - was that intentional? – nothingisnecessary Dec 06 '17 at 14:55
  • @nothingisnecessary case sensitive Yes, not looking for highlight, if anything I'd use it as a tool to get the strings i want, but I don't care about highlighting. I'm writing my own client side translation library for wordpress because i'm frustrated by current plugins which are good at one thing and bad at another. Since it's wordpress, I dont have access to all strings from source code, so I need an agnostic way to search for and manipulate the strings I want, as described in the question. Why agnostic? because I need to use it on many websites – Lynob Dec 06 '17 at 15:05

3 Answers3

3

Dave Gilem's replacer is a good start, but you need to recursively traverse through the DOM and apply it only to text nodes. This solution relies on jQuery. While DOM traversing using jQuery.each() is much slower, it's by far the easiest way to create a new HTML node in place of a matching string.

/**
  * replace text with <span> according to the wrapList
  * @param {Object} $elem jQuery DOM Object of a text node
  */
function replaceText($elem) {
    var pattern,
        textcontent  = $elem.text(),
        wrapList = {
            "fname": "First name",
            "lname": "Last name",
            "email": "Email"
            };

    for (const key in wrapList) {
        if (wrapList.hasOwnProperty(key)) {
            pattern = new RegExp(wrapList[key], 'g');
            // as we do a DOM manipulation, we only want to do it if necessary
            if (pattern.test(textcontent)) {
                // replace the text node with new text. $& in JS equals $0 in other languages
                textcontent = textcontent.replace(pattern,'<span data-string="'+key+'">$&</span>');
            }            
        }
    }
    // a change has been made
    if (textcontent != $elem.text()) {
        $elem.replaceWith(textcontent);
    }
}

/**
  * loop through DOM nodes recursively and call replaceText() on text nodes
  * @param {Object} $elem jQuery DOM Object
  */
function replaceRecursive($elem) {
    // get the nodeType of the object's DOM node
    var nt = $elem.get(0).nodeType;
    if (nt === 3) {
        // type 3 is a text node: replace the text
        replaceText($elem);
    } else if(nt === 1) {
        // type 1 is a DOM element: loop through the child objects recursively
        $elem.contents().each(function () {
            replaceRecursive($(this));
        });
    }
}

// start with the <body> tag
replaceRecursive($('body'));

jsfiddle

EDIT: There was a bug: Using replaceWith() in the for loop always updated the content. If there were multiple occurrences of search terms in the node, it only replaced the last one.

masterfloda
  • 2,908
  • 1
  • 16
  • 27
  • well havent tested it and dont have time to, since the bounty is going to expire anyway, im going to give it to you, since your solution seems better, and im gonna accept your answer. If someone comes up with a better solution im going to accept his. but the bounty is yours since it will expire while im sleeping – Lynob Dec 06 '17 at 21:48
  • OP stated in the comments "case sensitive". But you can easily make it insensitive by adding the `i` flag to the regex: `pattern = new RegExp(wrapList[key], 'ig');`. But there was a bug with multiple occurrences in one node. I fixed it and updated the answer and the fiddle. – masterfloda Dec 07 '17 at 18:22
  • oops i misread. much better now, tested with random page from wikipedia https://jsfiddle.net/rgeL5oaw/ – nothingisnecessary Dec 07 '17 at 20:25
-1

Similar to what xdumaine posted but a little more modular.

<div class='container'>
    First name <input type="text" name="fname">
    Last name 
    Email
  </div>

and calling the following function

function checkWrap($targetEl){
    var element, newText, pattern,
        wrapList = {
            "fname": "First name",
            "lname": "Last name",
            "email": "Email"
            },
        targetStr = $targetEl.html();
    for (const key in wrapList) {
        if (wrapList.hasOwnProperty(key)) {
            element = wrapList[key];
            pattern = new RegExp(element, 'g');
            newText = targetStr.replace(pattern, '<span data-string="'+key+'">'+element+'</span>');
            targetStr = newText;
        }
    }
    $targetEl.html(newText);
}
checkWrap($(".container"));

Should accomplish what you're after and allow you to decide to taget the whole body, a single element, or a set of elements by changing the taget elemnt you pass to the function call. (may need some optimization to the method but you should get the idea).

Dave Gillem
  • 476
  • 4
  • 10
  • @lynob did that work for you? There are a few issues and holes in this solution such as an inadvertent match inside a tag (e.g. attribute) but as long as you target only the areas you need and/or limit your wrapList array of items to specific strings unlikely to be found as an attribute, it may not be an issue for your specific use case. – Dave Gillem Dec 05 '17 at 18:28
  • 1
    limiting would mean knowing something about the html structure (not "agnostic" per OP) and using regex seems very dangerous, like you said because you could replace attributes or other data or even script that is not text content. – nothingisnecessary Dec 06 '17 at 14:13
-2

You can modify the document's body's innerHTML property.

document.body.innerHTML = document.body.innerHTML.replace(myString, '<span data-string="' + myKey + '">' + myString + '</span>');

Note that for this to be global, you'd need a regex instead of a string for the first param to replace, but you can make it a generic one with no need to be specific to the page structure.

xdumaine
  • 10,096
  • 6
  • 62
  • 103
  • 1
    Why the downvote? OP might be wanting to do a silly thing, but this does technically answer his question. – xdumaine Nov 27 '17 at 20:41
  • 3
    What about multiple instances? What about messing up attributes.? What about removing event listeners? Numerous potential gotchas here none of which you mention – charlietfl Nov 27 '17 at 20:41
  • 1
    @charlietfl you're not supposed to vote based on code robustness and overall patterns. My answer addresses a specific need of a specific question. – xdumaine Nov 27 '17 at 20:42
  • 1
    Only in a hackish way with no caveats mentioned – charlietfl Nov 27 '17 at 20:44