iamacup / react-native-markdown-display

React Native 100% compatible CommonMark renderer
MIT License
590 stars 169 forks source link

What is the recommended way to implement hashtags/mentions (regex)? #154

Closed DRogozenski closed 10 months ago

DRogozenski commented 3 years ago

I will describe 3 solutions I have tried so far below, but a deeper knowledge of the project is required to figure out the best method. I'm going to leave the info I've found because I'm sure more will need this in the future, so I think it'd be helpful to share anything that might be of use

To start, the example app in the original branch of this project, using linkify-it works https://github.com/mientjan/react-native-markdown-renderer/blob/260fad084a11006201cc657fdc64f54d4625fd38/example/App.js You can use a custom rule and styles to highlight the text and provide custom onPress functionality, as I've done here (This solution/code works):


import React from 'react';
import {
  SafeAreaView,
  StyleSheet,
  ScrollView,
  View,
  Text,
  StatusBar,
  Alert,
} from 'react-native';

import {
  Header,
  LearnMoreLinks,
  Colors,
  DebugInstructions,
  ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';

import Markdown, {MarkdownIt} from 'react-native-markdown-display';

const rules = {
  link: (node, children, parent, styles) => {
    return (
      <Text key={node.key} style={styles.link} onPress={() => onPressMention(children, node.attributes.href) }>
        {children}
      </Text>
    );
  },
};

const onPressMention = (e, mention) => {
    const msg = `Mention: "${mention}"`;
    if (Platform.OS !== "web") {
      if(mention.charAt(0) == "@"){
        Alert.alert(msg + "custom @");
      }else 
      if(mention.charAt(0) == "#"){
        Alert.alert(msg + "custom #");
      }else{
        Alert.alert(msg);
      }
    }
};

const markdownItInstance = 
    MarkdownIt({typographer: true, linkify: true});

// add twitter mention handler see https://github.com/markdown-it/linkify-it#example-2-add-twitter-mentions-handler where this is from.
markdownItInstance.linkify.add('@', {
  validate: function (text, pos, self) {
    var tail = text.slice(pos);

    if (!self.re.twitter) {
      self.re.twitter =  new RegExp(
        '^([a-zA-Z0-9_]){1,15}(?!_)(?=$|' + self.re.src_ZPCc + ')'
      );
    }
    if (self.re.twitter.test(tail)) {
      // Linkifier allows punctuation chars before prefix,
      // but we additionally disable `@` ("@@mention" is invalid)
      if (pos >= 2 && tail[pos - 2] === '@') {
        return false;
      }
      return tail.match(self.re.twitter)[0].length;
    }
    return 0;
  },
  normalize: function (match) {
    //match.url = 'https://twitter.com/' + match.url.replace(/^@/, '');
  }
});

markdownItInstance.linkify.add('#', {
  validate: function (text, pos, self) {
    var tail = text.slice(pos);

    if (!self.re.twitter) {
      self.re.twitter =  new RegExp(
        '^([a-zA-Z0-9_]){1,15}(?!_)(?=$|' + self.re.src_ZPCc + ')'
      );
    }
    if (self.re.twitter.test(tail)) {
      // Linkifier allows punctuation chars before prefix,
      // but we additionally disable `#` ("##mention" is invalid)
      if (pos >= 2 && tail[pos - 2] === '#') {
        return false;
      }
      return tail.match(self.re.twitter)[0].length;
    }
    return 0;
  },
  normalize: function (match) {
    //match.url = 'https://twitter.com/' + match.url.replace(/^#/, '');
  }
});

const copy = `Headings

  # h1 Heading 8-)
  ## h2 Heading
  ### h3 Heading

  ##Test!
  @@TEST@2
  #yolo #lol!
  @mention @user @lol12345

  Images

  ![Minion](https://octodex.github.com/images/minion.png)
  ![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")

  Like links, Images also have a footnote style syntax

  ![Alt text][id]

  With a reference later in the document defining the URL location:

  [id]: https://octodex.github.com/images/dojocat.jpg  "The Dojocat"

  Typographic Replacements

    Enable typographer option to see result.

    (c) (C) (r) (R) (tm) (TM) (p) (P) +-

    test.. test... test..... test?..... test!....

    !!!!!! ???? ,,  -- ---

    "Smartypants, double quotes" and 'single quotes'
  `;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
          <Markdown markdownit={markdownItInstance} rules={rules} style={styles1} maxTopLevelChildren={7}>
            {copy}
           </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

const styles1 = StyleSheet.create({
  link: {
    fontWeight: '400',
    color: '#14B4E5',
    fontSize: 14,
  },
});

const styles = StyleSheet.create({
  scrollView: {
    backgroundColor: Colors.lighter,
  },
  engine: {
    position: 'absolute',
    right: 0,
  },
  body: {
    backgroundColor: Colors.white,
  },
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
    color: Colors.black,
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
    color: Colors.dark,
  },
  highlight: {
    fontWeight: '700',
  },
  footer: {
    color: Colors.dark,
    fontSize: 12,
    fontWeight: '600',
    padding: 4,
    paddingRight: 12,
    textAlign: 'right',
  },
  linkify: {
    fontWeight: '400',
    color: '#14B4E5',
    fontSize: 14,
  },
});

export default App;

One of the options I've also looked into is using a markdown-it plugin for regex, but I haven't had success with it yet, and speed is also a concern (as the author of the plugin warns). This is the most recent code I tried for that (I can see the HTML is a problem, but I'm not going to spend more time trying to fix this solution):


import React from 'react';
import {
  SafeAreaView,
  StyleSheet,
  ScrollView,
  View,
  Text,
  StatusBar,
} from 'react-native';

import {
  Header,
  LearnMoreLinks,
  Colors,
  DebugInstructions,
  ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';

import Markdown, {MarkdownIt} from 'react-native-markdown-display';
import createPlugin from '@gerhobbelt/markdown-it-regexp';

const plugin = createPlugin(
  // regexp to match
  /@(\w+)/,

  // this function will be called when something matches
  function (match, setup, options) {
    let url = 'http://example.org/u/' + match[1];

    return '<a href="' + setup.escape(url) + '">'
         + setup.escape(match[1])
         + '</a>';
  }
);

const markdownItInstance = 
    MarkdownIt({typographer: true})
      .use(plugin);

const copy = `# h1 Heading 8-)

**This is some bold text!**

This is normal text

#yolo lol @mention haha
`;

const App: () => React$Node = () => {
  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView>
        <ScrollView
          contentInsetAdjustmentBehavior="automatic"
          style={{height: '100%'}}
        >
          <Markdown markdownit={markdownItInstance}
              style={{
                  hashtag: {
                    color: 'blue',
                  }
                }}
              rules={{
                  hashtag: (node, children, parent, styles) =>{
                    // examine the node properties to see what video we need to render
                    console.log(node); // expected output of this is in readme.md below this code snip

                    return (<Text key={node.key} style={styles.hashtag}>
                      {node.content}
                    </Text>);
                  }

                }}
                >
            {copy}
          </Markdown>
        </ScrollView>
      </SafeAreaView>
    </>
  );
};

const styles = StyleSheet.create({
  scrollView: {
    backgroundColor: Colors.lighter,
  },
  engine: {
    position: 'absolute',
    right: 0,
  },
  body: {
    backgroundColor: Colors.white,
  },
  sectionContainer: {
    marginTop: 32,
    paddingHorizontal: 24,
  },
  sectionTitle: {
    fontSize: 24,
    fontWeight: '600',
    color: Colors.black,
  },
  sectionDescription: {
    marginTop: 8,
    fontSize: 18,
    fontWeight: '400',
    color: Colors.dark,
  },
  highlight: {
    fontWeight: '700',
  },
  footer: {
    color: Colors.dark,
    fontSize: 12,
    fontWeight: '600',
    padding: 4,
    paddingRight: 12,
    textAlign: 'right',
  },
});

export default App;

Prior to trying those, the other option I tried was using a branch of somebody that tried to edit this package to add it right into the renderRules: https://github.com/gimwee/react-native-markdown-display/commit/992ec2e5363187f453c2d638c720a51e4d4067dd but this attempt, unfortunately, didn't change anything and resulted in the original functionality upon testing. The branch doesn't seem to work, but that type of solution may be salvageable if you want to look further into it.

I would very much appreciate any insight as to the best way to implement tagging and mentions. A short explanation would be ideal, if you know another solution that works please don't hesitate to share. Also if you have any comments about the solutions I've tried already, tell me :)