What is a simple solution to editable-by-end-user

2019-08-19 05:35发布

问题:

Here's the problem I'm trying to solve: a user can display a custom 4-character title for something on a web site, e.g. NEWS. I want to add support for a way that a user can specify icons from Font Awesome as well.

I was thinking about using brackets, e.g. the user would write [camera]pic and that would be translated to <i class="icon-camera"></i>pic to be displayed with the proper Font Awesome icon. However, I would also like to be able to escape the markup, so that (e.g.) f[[x] would be printed as f[x], not f[<i class="icon-x"></i>. The text placeholders will never be nested, but they maybe be adjacent (e.g. [star][star][star][star]). I struggled to solve this using regular expressions*, and concluded that regular expressions probably aren't the appropriate solution to the problem.

Is there a simple solution to this kind of problem that can be cleanly implemented in Javascript and Ruby? Alternatively, is there another simple way to represent these kinds of text placeholders that meet my sequential and escapable requirements (e.g. ${camera}pic instead)? Or will I have to parse it by hand, one character at a time?


* As for the regular expressions I've tried: \[(\w+)\] is simple but matches when it shouldn't on f[[x]. (\A|[^\[])\[(\w+)\] is passes f[[x] but fails for every other placeholder in [x][y][z].

Here are my test cases. Assuming a simple transform of replacing the placeholder the placeholder text prefixed with a $, then:

describe '#to_var' do
   it { helper.to_var('abcd').should == 'abcd' }
   it { helper.to_var('[foo][bar][baz]').should == '$foo$bar$baz' }
   it { helper.to_var('[[x]').should == '[x]' }
   it { helper.to_var('<[[x]>').should == '<[x]>' }
   it { helper.to_var('<[x]>').should == '<$x>' }   
end

The closest regex I came up with was:

 icon_code_regex =  %r(
    (\A # beginning of string
     | # or
     [^\[]) # not a left bracket
    \[ # literal left bracket
    (\w+) # the good stuff
    \] # literal right bracket
  )x

str.gsub(icon_code_regex, '\1$\2').gsub('[[', '[')

which fails the [foo][bar][baz] case.

回答1:

Javascript solution:

var str = 'abcd [foo][bar][baz] [[x] <[[x]> <[x]>';

str = str.replace( /(\[)?\[(\w+)\]/g, function ( match, escaped, icon ) {
    return escaped ? '[' + icon + ']' : '$' + icon;
});

// "abcd $foo$bar$baz [x] <[x]> <$x>"


回答2:

Just for demonstration, here is a more elegant JS solution which uses replace:

var output = str.replace(/\[(\w+)\]/g, function(match, icon, offset) {
    if (offset>0 && str.charAt(offset-1) == '[') // if previous existed and was [
        return match.slice(1); // return the match but without opening [
    // else
    return '<i class="icon-' + icon + '" />'; // or whatever you want to do
});


回答3:

Solution for Ruby

You can use look-behind to prevent the substring [word] in [[word] from matching:

(?<!\[)\[(\w+)\]

The look-behind (?<!\[) simply checks [ does not appear before the string we want to match.

DEMO

Solution for JS

For JS workaround, since it doesn't have look-behind:

// Match one non-opening-bracket character, then the [word],
// but the closing bracket is not consumed, for the case
// of consecutive [word1][word2]
var regex = /(^|[^\[])\[(\w+)(?=\])/g;
var arr;

var output = "";
var lastAppend = 0;

while ((arr = regex.exec(inputString)) !== null) {
    // Append substring from index of lastAppend to right before opening [
    // lastAppend will either be at beginning of the string (0)
    // OR right after closing ] from previous match
    output += inputString.substring(lastAppend, arr.index + arr[1].length);
    output += "$" + arr[2];

    // Need to offset by 1 to skip the closing ] (not consumed)
    lastAppend = regex.lastIndex + 1;
}

output += inputString.substring(lastAppend);

It is quite ugly. I am not sure if there is a more elegant way.