Help me with a programming task. I have code written in JS. It is a BBcode parser, it parses BBcode and outputs JSON that can be used in the JS library pdfmake.
When it runs in iOS it throws an error because Apple's devices don't support regex lookback yet. How can I change this code so it does not use regex lookback?

Here is the code:

function getParser(
    fontsDictionary,
    addImageIntoDictionary,
    errorHandler = () => {},
    codeLayout = { fillColor: () => '#23241f', vLineWidth: () => 0, hLineWidth: () => 0 },
    quoteLayout = { vLineWidth: () => 5, vLineColor: () => '#ccc', hLineWidth: () => 0, paddingLeft: () => 20 }
  ) {
    // Prototypes
  
    String.prototype.findClosingTag = function (tagType) {
      const tags = new Tags();
  
      const openingTagPattern = tags.isANotParameterizedTag(tagType)
        ? Patterns.getNotParameterizedOpeningTag([tagType], 'g')
        : Patterns.getOpeningTag([tagType], 'g');
  
      const closingTagPattern = Patterns.getClosingTag([tagType], 'g');
  
      const openingTagPositions = [...this.matchAll(openingTagPattern)].map((match) => match.index);
  
      const closingTagPositions = [...this.matchAll(closingTagPattern)].map((match) => match.index);
  
      if (closingTagPositions.length === 0 || openingTagPositions.length === 0) {
        return -1;
      }
  
      if (closingTagPositions.length === 1 || openingTagPositions.length === 1) {
        const [position] = closingTagPositions;
        return position;
      }
  
      for (let position = 0; position < closingTagPositions.length; position++) {
        if (openingTagPositions[position + 1] > closingTagPositions[position]) {
          return closingTagPositions[position];
        }
      }
  
      const lastPosition = closingTagPositions[closingTagPositions.length - 1];
  
      return lastPosition;
    };
  
    String.prototype.chopString = function (openingTagsPattern, hasClosingTag = true) {
      let string = String(this);
  
      let hasStyles = string.match(openingTagsPattern);
  
      if (!hasStyles) return string;
  
      const choppedString = [];
  
      while (hasStyles) {
        const [tag, tagType] = hasStyles;
        const { index: openingTagPosition } = hasStyles;
  
        // If there is some text before the tag
        if (openingTagPosition > 0) {
          const firstStringPart = string.slice(0, openingTagPosition);
          choppedString.push(firstStringPart);
        }
  
        const closingTagLength = hasClosingTag ? `[/${tagType}]`.length : 0;
  
        const closingTagPosition = hasClosingTag ? string.findClosingTag(tagType) : -1;
  
        if (hasClosingTag && closingTagPosition === -1) {
          return [...choppedString, string];
        }
  
        // Calculate where the chop needs to stop
        const endPosition = hasClosingTag ? closingTagPosition + closingTagLength : openingTagPosition + tag.length;
  
        // Take the tag part of the string and put it into the array
        const tagStringPart = string.slice(openingTagPosition, endPosition);
        choppedString.push(tagStringPart);
  
        // The rest of the string
        const restStringPart = string.slice(endPosition);
  
        // If there isn't a string rest part
        if (!restStringPart) {
          break;
        } else {
          string = restStringPart;
          hasStyles = string.match(openingTagsPattern);
  
          if (!hasStyles) choppedString.push(restStringPart);
        }
      }
  
      return choppedString;
    };
  
    String.prototype.isOpenTagComeFirst = function (tag) {
      const tags = new Tags();
  
      const openTag = tags.isANotParameterizedTag(tag) ? `[${tag}]` : `[${tag}`;
      const closeTag = `[/${tag}]`;
  
      return this.indexOf(openTag) <= this.indexOf(closeTag);
    };
  
    String.prototype.isAListString = function () {
      return this.search(/^\[(?:ul|ol)(?:.*?)\]/s) !== -1;
      // return this.startsWith('[ul]') || this.startsWith('[ol]');
    };
  
    String.prototype.thereIsAList = function () {
      return this.search(/\[(?:ul|ol)(.*?)\]/s) !== -1;
      // return this.includes('[ul]') || this.includes('[ol]');
    };
  
    // Helpers
  
    class Tags {
      constructor() {
        this.tags = {
          styles: ['b', 'i', 'u', 's', 'sup', 'sub', 'font', 'color', 'size', 'url', 'email', 'highlight'],
          media: ['img'],
          list: ['ul', 'ol', 'li'],
          title: ['h1', 'h2', 'h3', 'h4'],
          extra: ['code', 'quote'],
          alignment: ['left', 'center', 'right', 'justify'],
          withoutClosing: ['hr'],
        };
      }
  
      getAllTags(...except) {
        const tags = Object.values(this.tags).flat();
  
        return tags.filter((tag) => !except.includes(tag));
      }
  
      getBreakLineTags(...except) {
        const { list, alignment, withoutClosing, title, extra, media } = this.tags;
        const tags = [...list, ...alignment, ...withoutClosing, ...title, ...extra, ...media];
  
        if (!except.includes('li')) except.push('li');
  
        return tags.filter((tag) => !except.includes(tag));
      }
  
      getNotParameterizedTag(...except) {
        const { styles, title, extra } = this.tags;
        const tags = [...styles, ...title, ...extra];
  
        except.push('font', 'color', 'size', 'url', 'email', 'highlight');
  
        return tags.filter((tag) => !except.includes(tag));
      }
  
      isANotParameterizedTag(tag) {
        return this.getNotParameterizedTag().includes(tag);
      }
    }
  
    class Patterns {
      static prepareTags(...tags) {
        return tags.sort((a, b) => b.length - a.length).join('|');
      }
  
      static getOpeningTag(tagTypes, flags = '') {
        const tags = Patterns.prepareTags(...tagTypes);
        return new RegExp(`\\[(${tags})=?(.*?)\\]`, flags);
      }
  
      static getClosingTag(tagTypes, flags = '') {
        const tags = Patterns.prepareTags(...tagTypes);
        return new RegExp(`\\[\\/(${tags})\\]`, flags);
      }
  
      static getFullTag(tagTypes, flags = '') {
        const tags = Patterns.prepareTags(...tagTypes);
        return new RegExp(`^\\[(${tags})=?(.*?)\\](.*)\\[\\/\\1\\]$`, flags);
      }
  
      static getBreakLineBeforeTag(tagTypes, flags = '') {
        const tags = Patterns.prepareTags(...tagTypes);
        return new RegExp(`(?<=\\[\\/?(.*?)\\])\n+(?=\\[\\/?(?:${tags})\\])`, flags);
      }
  
      static getBreakLineAfterTag(tagTypes, flags = '') {
        const tags = Patterns.prepareTags(...tagTypes);
        return new RegExp(`(?<=\\[\\/?(?:${tags})\\])\n`, flags);
      }
  
      static getNotParameterizedOpeningTag(tagTypes, flags = '') {
        const tags = Patterns.prepareTags(...tagTypes);
        return new RegExp(`\\[(${tags})\\]`, flags);
      }
    }
  
    class ParserHelper {
      static pipe(functions, initialValue) {
        return functions.reduce((a, fn) => fn(a), initialValue);
      }
  
      static getHEXColor(color) {
        if (color.startsWith('rgb')) {
          const [r, g, b] = color.match(/\d+/g).map(Number);
  
          return [r, g, b].reduce((a, b) => a + b.toString(16).padStart(2, '0'), '#');
        }
        return color;
      }
  
      static generateRandomValues(length) {
        const number = Math.floor(Math.random() * 10 ** length);
  
        return String(number).padStart(length, '0');
      }
  
      static getImageProperties(value) {
        const input = value.trim();
  
        if (input.includes('x')) {
          const [width, height] = input.split('x').map(Number);
  
          const options = {};
          if (width) options.width = width;
          if (height) options.height = height;
  
          return options;
        } else {
          const properties = input.split(' ').map((property) => {
            const [key, value] = property.split('=');
  
            return [key, Number(value)];
          });
  
          return Object.fromEntries(properties);
        }
      }
  
      static getNewLineByTag(text, tag, value, options = {}) {
        let newLine = {};
  
        //Checking the closeTag type
        switch (tag) {
          case 'center':
          case 'left':
          case 'right':
          case 'justify':
            newLine = { text, ...options, alignment: tag };
            break;
  
          case 'size': {
            const sizes = [10, 13, 16, 18, 24, 32, 48];
  
            const size = Number(value);
  
            newLine = { text, ...options, fontSize: sizes[size - 1] };
            break;
          }
  
          case 'color': {
            const color = ParserHelper.getHEXColor(value);
            newLine = { text, ...options, color };
            break;
          }
  
          case 'b': {
            newLine = { text, ...options, bold: true };
            break;
          }
  
          case 'i': {
            newLine = { text, ...options, italics: true };
            break;
          }
  
          case 'u': {
            newLine = { text, ...options, decoration: 'underline' };
            break;
          }
  
          case 's': {
            newLine = { text, ...options, decoration: 'lineThrough' };
            break;
          }
  
          case 'sup': {
            const sup = { offset: '15%' };
            newLine = { text, ...options, sup };
            break;
          }
  
          case 'sub': {
            const sub = { offset: '15%' };
            newLine = { text, ...options, sub };
            break;
          }
  
          case 'url': {
            const link = value;
  
            const decoration = 'underline';
            const color = 'blue';
  
            newLine = { text, ...options, link, decoration, color };
            break;
          }
  
          case 'email': {
            const email = value;
  
            const link = 'mailto:' + email;
            const decoration = 'underline';
            const color = 'blue';
  
            newLine = { text, ...options, link, decoration, color };
            break;
          }
  
          case 'font': {
            const font = value.replace(/\"/g, '');
  
            if (fontsDictionary && fontsDictionary[font]) {
              options.font = font;
            } else {
              const error = new Error(`Font not found: ${font}\nPlease check if the font was loaded before use it`);
              errorHandler(error);
            }
  
            newLine = { text, ...options };
            break;
          }
  
          case 'ul': {
            newLine = { ul: text, ...options };
            break;
          }
  
          case 'ol': {
            newLine = { ol: text, ...options };
            break;
          }
  
          case 'li': {
            if (text.thereIsAList()) {
              newLine = { stack: text, ...options };
            } else {
              newLine = { text, ...options };
            }
            break;
          }
  
          case 'h1': {
            newLine = { text, ...options, fontSize: 26 };
            break;
          }
  
          case 'h2': {
            newLine = { text, ...options, fontSize: 20 };
            break;
          }
  
          case 'h3': {
            newLine = { text, ...options, fontSize: 16 };
            break;
          }
  
          case 'h4': {
            newLine = { text, ...options, fontSize: 13 };
            break;
          }
  
          case 'highlight': {
            const background = ParserHelper.getHEXColor(value);
            newLine = { text, ...options, background };
            break;
          }
  
          case 'code': {
            const parser = new BBCodeParser();
            const parsedText = parser.getParsedText(text);
  
            newLine = {
              layout: codeLayout,
              table: {
                widths: ['*'],
                body: [[{ text: parsedText, color: '#f8f8f2' }]],
              },
              ...options,
            };
            break;
          }
  
          case 'quote': {
            const parser = new BBCodeParser();
            const parsedText = parser.getParsedText(text);
  
            newLine = {
              layout: quoteLayout,
              table: {
                widths: ['*'],
                body: [[{ text: parsedText }]],
              },
              ...options,
            };
            break;
          }
  
          case 'img': {
            const link = text.startsWith('http') ? text : 'https:' + text;
  
            const imageName = ParserHelper.generateRandomValues(8) + '-image-' + text.slice(text.lastIndexOf('/') + 1);
  
            if (typeof addImageIntoDictionary === 'function') {
              addImageIntoDictionary(imageName, link);
            }
  
            const imgProperties = ParserHelper.getImageProperties(value);
  
            newLine = { image: imageName, ...options, ...imgProperties };
            break;
          }
        }
  
        return newLine;
      }
  
      static getOutsiderLineStyles(line, pattern, previousOptions = {}) {
        let { text, ol, ul, ...lineOptions } = line;
  
        if (typeof line === 'string') lineOptions = {};
  
        const targetString = text || ol || ul || line;
        const options = { ...previousOptions, ...lineOptions };
  
        let lineType = 'text';
        if (ul) lineType = 'ul';
        if (ol) lineType = 'ol';
  
        if (typeof targetString !== 'string') return line;
  
        const hasStyles = targetString.match(pattern);
  
        if (!hasStyles) return { [lineType]: targetString, ...options };
  
        const [match, tagType, value, innerText] = hasStyles;
  
        if (innerText.isOpenTagComeFirst(tagType)) {
          const newLine = ParserHelper.getNewLineByTag(innerText, tagType, value, options);
  
          if (targetString.isAListString()) return newLine;
  
          return ParserHelper.getOutsiderLineStyles(newLine, pattern);
        }
  
        return { [lineType]: targetString, ...options };
      }
  
      static getInsiderLineStyles(line, openingTagsPattern, outsiderTagPattern) {
        let { text, ul, ol, stack, ...options } = line;
  
        if (typeof line === 'string') options = {};
  
        const targetString = text || ol || ul || stack || line;
  
        let lineType = 'text';
        if (ul) lineType = 'ul';
        if (ol) lineType = 'ol';
        if (stack) lineType = 'stack';
  
        if (typeof targetString !== 'string') return line;
  
        const hasStyles = targetString.match(openingTagsPattern);
  
        if (!hasStyles) return { [lineType]: targetString, ...options };
  
        // Verify if there's the closing tag
        const [match, tag] = hasStyles;
        const closingTagPattern = Patterns.getClosingTag([tag]);
  
        // If the closing tag is not find, to avoid infinite recursion we break the flow here
        const hasClosingTag = targetString.match(closingTagPattern);
  
        if (!hasClosingTag) return { [lineType]: targetString, ...options };
  
        // If its a stack item first break the internal lists then break the styles
        const listsOpeningTagsPattern = Patterns.getOpeningTag(['ul', 'ol']);
  
        const stringArray = !stack
          ? targetString.chopString(openingTagsPattern)
          : targetString.chopString(listsOpeningTagsPattern);
  
        const resultingLine = stringArray
          .map((item) => ParserHelper.getOutsiderLineStyles(item, outsiderTagPattern, options))
          .map((item) => ParserHelper.getInsiderLineStyles(item, openingTagsPattern, outsiderTagPattern));
  
        return { [lineType]: resultingLine, ...options };
      }
  
      static fixOlListsHelper(element) {
        const { ol, ...options } = element;
  
        let list = ol || element;
  
        if (!list || !(list instanceof Array) || !list.some(({ ol }) => Boolean(ol))) return element;
  
        const newList = [];
  
        let test = true;
  
        while (test) {
          const listIndex = list.findIndex(({ ol }) => Boolean(ol));
  
          if (listIndex > 1) {
            newList.push(...list.slice(0, listIndex - 1));
          }
  
          const previousItem = list[listIndex - 1];
          const item = list[listIndex];
  
          newList.push({ stack: [previousItem, ParserHelper.fixOlListsHelper(item)] });
  
          const listRest = list.slice(listIndex + 1);
  
          test = listRest.some(({ ol }) => Boolean(ol));
          list = listRest;
  
          if (!test) newList.push(...listRest);
        }
  
        return { ol: newList, ...options };
      }
    }
  
    // Parser
  
    class BBCodeParser {
      constructor() {
        this.functions = [
          this.prepareContent,
          this.breakLineTagsHandler,
          this.horizontalRuleTagHandler,
          this.horizontalRuleTagParser,
          this.outsiderStylesParser,
          this.insiderStylesParser,
          this.fixOlLists,
        ].map((fn) => fn.bind(this));
  
        this.tags = new Tags();
      }
  
      prepareContent(contents = '') {
        if (!contents || typeof contents !== 'string') {

          return '';
          
        }
  
        const tags = [...this.tags.getBreakLineTags(), 'li'];
  
        const beforeTags = Patterns.getBreakLineBeforeTag(['ul', 'ol', 'li'], 'g');
        const afterTags = Patterns.getBreakLineAfterTag(tags, 'g');
  
        contents = contents.replace(/\[ml\]/g, '');
        contents = contents.replace(/\[\/ml\]/g, '\n');
  
        contents = contents.replace(/\n\[\/(center|justify|right|code)\]/g, (match, tag) => `[/${tag}]\n`);
  
        contents = contents.replace(/\[\/quote\]/g, (match) => match + '\n');
  
        contents = contents.replace(afterTags, '');
        contents = contents.replace(beforeTags, (match, tag) => {
          if (tags.includes(tag)) return match;
  
          return match.replace(/\n/, '');
        });
  
        return contents;
      }
  
      breakLineTagsHandler(contents) {
        if (!contents) return [];
  
        const breakLineTags = this.tags.getBreakLineTags('hr');
        const openingTagPattern = Patterns.getOpeningTag(breakLineTags);
  
        const result = contents.chopString(openingTagPattern);
  
        if (typeof result === 'string') return [result];
  
        return result;
      }
  
      horizontalRuleTagHandler(contents) {
        const openingTagPattern = Patterns.getOpeningTag(['hr']);
  
        return contents.map((line) => line.chopString(openingTagPattern, false)).flat();
      }
  
      horizontalRuleTagParser(contents) {
        return contents.map((line) => {
          if (line !== '[hr]') return line;
          return { canvas: [{ type: 'line', x1: 0, y1: 0, x2: 515, y2: 0, lineWidth: 1 }] };
        });
      }
  
      outsiderStylesParser(contents) {
        const tags = this.tags.getAllTags('hr');
        const pattern = Patterns.getFullTag(tags, 's');
  
        return contents.map((line) => ParserHelper.getOutsiderLineStyles(line, pattern));
      }
  
      insiderStylesParser(contents) {
        const tags = this.tags.getAllTags('hr');
  
        const openingTagPattern = Patterns.getOpeningTag(tags);
        const outsiderTagPattern = Patterns.getFullTag(tags, 's');
  
        return contents.map((line) => ParserHelper.getInsiderLineStyles(line, openingTagPattern, outsiderTagPattern));
      }
  
      fixOlLists(contents) {
        return contents.map(ParserHelper.fixOlListsHelper);
      }
  
      getParsedText(text) {
        return ParserHelper.pipe(this.functions, text);
      }
    }
  
    return new BBCodeParser();
  }