예제 #1
0
        ///<summary>Converts an image markup tag like [[img:myimage.jpeg]] to html.</summary>
        private static string TranslateEmailImages(string s)
        {
            //[[img:myimage.jpg]]------------------------------------------------------------------------------------------------------------
            MatchCollection matches = Regex.Matches(s, _odWikiImage);

            foreach (Match match in matches)
            {
                string imgName   = match.Value.Substring(match.Value.IndexOf(":") + 1).TrimEnd("]".ToCharArray());
                string imagePath = "";
                try {
                    imagePath = ImageStore.GetEmailImagePath();
                }
                catch (Exception ex) {
                    ex.DoNothing();
                    throw;
                }
                string fullPath = FileAtoZ.CombinePaths(imagePath, POut.String(imgName));
                if (CloudStorage.IsCloudStorage)
                {
                    //WebBrowser needs to have a local file to open, so we download the images to temp files.
                    OpenDentalCloud.Core.TaskStateDownload state = CloudStorage.Download(Path.GetDirectoryName(fullPath), Path.GetFileName(fullPath));
                    string tempFile = PrefC.GetRandomTempFile(Path.GetExtension(fullPath));
                    File.WriteAllBytes(tempFile, state.FileContent);
                    fullPath = tempFile;
                }
                s = s.Replace(match.Value, "<img src=\"" + fullPath + "\"></img>");                 //"\" />");
            }
            return(s);
        }
예제 #2
0
        ///<summary>Reprocess the original HL7 msgs for any MedLabs with PatNum 0, creates the embedded PDF files from the base64 text in the ZEF segments
        ///<para>The old method used when parsing MedLab HL7 msgs was to wait to extract these files until the msg was manually associated with a patient.
        ///Associating the MedLabs to a patient and reprocessing the HL7 messages using middle tier was very slow.</para>
        ///<para>The new method is to create the PDF files and save them in the image folder in a subdirectory called "MedLabEmbeddedFiles" if a patient
        ///isn't located from the details in the PID segment of the message.  Associating the MedLabs to a pat is now just a matter of moving the files to
        ///the pat's image folder and updating the PatNum columns.  All files are now extracted and stored, either in a pat's folder or in the
        ///"MedLabEmbeddedFiles" folder, by the HL7 service.</para>
        ///<para>This will reprocess all HL7 messages for MedLabs with PatNum=0 and replace the MedLab, MedLabResult, MedLabSpecimen, and MedLabFacAttach
        ///rows as well as create any embedded files and insert document table rows.  The document table rows will have PatNum=0, just like the MedLabs,
        ///if a pat is still not located with the details in the PID segment.  Once the user manually attaches the MedLab to a patient, all rows will be
        ///updated with the correct PatNum and the embedded PDFs will be moved to the pat's image folder.  The document.FileName column will contain the
        ///name of the file, regardless of where it is located.  The file name will be updated to a relevant name for the folder in which it is located.
        ///i.e. in the MedLabEmbeddedFiles directory it may be named 3YG8Z420150909100527.pdf, but once moved to a pat's folder it will be renamed to
        ///something like PatientAustin375.pdf and the document.FileName column will be the current name.</para>
        ///<para>If storing images in the db, the document table rows will contain the base64 text version of the PDFs with PatNum=0 and will be updated
        ///with the correct PatNum once associated.  The FileName will be just the extension ".pdf" until it is associated with a patient at which time it
        ///will be updated to something like PatientAustin375.pdf.</para></summary>
        public static int Reconcile()
        {
            if (RemotingClient.RemotingRole == RemotingRole.ClientWeb)
            {
                return(Meth.GetInt(MethodBase.GetCurrentMethod()));
            }
            string        command     = "SELECT * FROM medlab WHERE PatNum=0";
            List <MedLab> listMedLabs = Crud.MedLabCrud.SelectMany(command);

            if (listMedLabs.Count < 1)
            {
                return(0);
            }
            List <long> listMedLabNumsNew = new List <long>();        //used to delete old MedLab objects after creating these new ones from the HL7 message text
            int         failedCount       = 0;

            foreach (string relativePath in listMedLabs.Select(x => x.FileName).Distinct().ToList())
            {
                string fileTextCur = "";
                try {
                    if (PrefC.AtoZfolderUsed != DataStorageType.InDatabase)
                    {
                        fileTextCur = FileAtoZ.ReadAllText(FileAtoZ.CombinePaths(ImageStore.GetPreferredAtoZpath(), relativePath));
                    }
                }
                catch (Exception ex) {
                    ex.DoNothing();                    //To avoid a warning message.  The ex is needed to ensure all exceptions are caught.
                    failedCount++;
                    continue;
                }
                MessageHL7  msg = new MessageHL7(fileTextCur);
                List <long> listMedLabNumsCur = MessageParserMedLab.Process(msg, relativePath, false);           //re-creates the documents from the ZEF segments
                if (listMedLabNumsCur == null || listMedLabNumsCur.Count < 1)
                {
                    failedCount++;
                    continue;                    //not sure what to do, just move on?
                }
                listMedLabNumsNew.AddRange(listMedLabNumsCur);
                MedLabs.UpdateFileNames(listMedLabNumsCur, relativePath);
            }
            //Delete all MedLabs, MedLabResults, MedLabSpecimens, and MedLabFacAttaches except the ones just created
            //Don't delete until we successfully process the messages and have valid new MedLab objects
            foreach (MedLab medLab in listMedLabs)
            {
                failedCount += DeleteLabsAndResults(medLab, listMedLabNumsNew);
            }
            return(failedCount);
        }
예제 #3
0
        ///<summary>If this is middle tier, pass in null.</summary>
        public static void LoadAllPlugins(Form host)
        {
            //No need to check RemotingRole; no call to db.
            List <PluginContainer> listPlugins = new List <PluginContainer>();

            //Loop through all programs that are enabled with a plug-in dll name set.
            foreach (Program program in Programs.GetWhere(x => x.Enabled && !string.IsNullOrEmpty(x.PluginDllName)))
            {
                string dllPath = ODFileUtils.CombinePaths(Application.StartupPath, program.PluginDllName);
                if (RemotingClient.RemotingRole == RemotingRole.ServerWeb)
                {
                    dllPath = ODFileUtils.CombinePaths(System.Web.HttpContext.Current.Server.MapPath(null), program.PluginDllName);
                }
                //Check for the versioning trigger.
                //For example, the plug-in might be entered as MyPlugin[VersionMajMin].dll. The bracketed section will be removed when loading the dll.
                //So it will look for MyPlugin.dll as the dll to load. However, before it loads, it will look for a similar dll with a version number.
                //For example, if using version 14.3.23, it would look for MyPlugin14.3.dll.
                //If that file is found, it would replace MyPlugin.dll with the contents of MyPlugin14.3.dll, and then it would load MyPlugin.dll as normal.
                if (dllPath.Contains("[VersionMajMin]"))
                {
                    Version vers = new Version(Application.ProductVersion);
                    string  dllPathWithVersion = dllPath.Replace("[VersionMajMin]", vers.Major.ToString() + "." + vers.Minor.ToString());
                    dllPath = dllPath.Replace("[VersionMajMin]", "");                 //now stripped clean
                    if (File.Exists(dllPathWithVersion))
                    {
                        File.Copy(dllPathWithVersion, dllPath, true);
                    }
                    else
                    {
                        //try the Plugins folder
                        if (PrefC.AtoZfolderUsed != DataStorageType.InDatabase)                       //must have an AtoZ folder to check
                        {
                            string dllPathVersionCentral = FileAtoZ.CombinePaths(ImageStore.GetPreferredAtoZpath(), "Plugins",
                                                                                 program.PluginDllName.Replace("[VersionMajMin]", vers.Major.ToString() + "." + vers.Minor.ToString()));
                            if (FileAtoZ.Exists(dllPathVersionCentral))
                            {
                                FileAtoZ.Copy(dllPathVersionCentral, dllPath, FileAtoZSourceDestination.AtoZToLocal, doOverwrite: true);
                            }
                        }
                    }
                }
                //We now know the exact name of the dll for the plug-in.  Check to see if it is present.
                if (!File.Exists(dllPath))
                {
                    continue;                    //Nothing to do.
                }
                //The dll was found, try and load it in.
                PluginBase plugin  = null;
                Assembly   ass     = null;
                string     assName = "";
                try {
                    ass     = Assembly.LoadFile(dllPath);
                    assName = Path.GetFileNameWithoutExtension(dllPath);
                    string typeName = assName + ".Plugin";
                    Type   type     = ass.GetType(typeName);
                    plugin      = (PluginBase)Activator.CreateInstance(type);
                    plugin.Host = host;
                }
                catch (Exception ex) {
                    //Never try and show message boxes when on the middle tier, there is no UI.  We should instead log to a file or the event viewer.
                    if (RemotingClient.RemotingRole != RemotingRole.ServerWeb)
                    {
                        //Notify the user that their plug-in is not loaded.
                        MessageBox.Show("Error loading Plugin:" + program.PluginDllName + "\r\n" + ex.Message);
                    }
                    continue;                    //Don't add it to plugin list.
                }
                //The plug-in was successfully loaded and will start getting hook notifications.  Add it to the list of loaded plug-ins.
                PluginContainer container = new PluginContainer();
                container.Plugin     = plugin;
                container.ProgramNum = program.ProgramNum;
                container.Assemb     = ass;
                container.Name       = assName;
                listPlugins.Add(container);
            }
            ListPlugins = listPlugins;
        }
예제 #4
0
        ///<summary>Throws exceptions.  Creates a new file inside of the email attachment path (inside OpenDentImages) and returns an EmailAttach object
        ///referencing the new file.  If isOutbound is true, then the file will be saved to the "Out" subfolder, otherwise the file will be saved to the
        ///"In" subfolder.  The displayFileName will always contain valid file name characters, because it is either a hard coded value or is based on an
        ///existing valid file name.  If a file already exists matching the actualFileName, then an exception will occur.  Set actualFileName to empty
        ///string to generate a unique actual file name.  If the actual file name is generated, then actual file name will end with the displayFileName,
        ///so that the actual files are easier to locate and have the same file extension as the displayedFileName.</summary>
        public static EmailAttach CreateAttach(string displayedFileName, string actualFileName, byte[] arrayData, bool isOutbound)
        {
            //No need to check RemotingRole; no call to db.
            EmailAttach emailAttach = new EmailAttach();

            emailAttach.DisplayedFileName = displayedFileName;
            if (String.IsNullOrEmpty(emailAttach.DisplayedFileName))
            {
                //This could only happen for malformed incoming emails, but should not happen.  Name uniqueness is virtually guaranteed below.
                //The actual file name will not have an extension, so the user will be asked to pick the program to open the attachment with when
                //the attachment is double-clicked.
                emailAttach.DisplayedFileName = "attach";
            }
            string attachDir = GetAttachPath();
            string subDir    = "In";

            if (isOutbound)
            {
                subDir = "Out";
            }
            if (!CloudStorage.IsCloudStorage && !Directory.Exists(ODFileUtils.CombinePaths(attachDir, subDir)))
            {
                Directory.CreateDirectory(ODFileUtils.CombinePaths(attachDir, subDir));
            }
            if (String.IsNullOrEmpty(actualFileName))
            {
                while (String.IsNullOrEmpty(emailAttach.ActualFileName) ||
                       FileAtoZ.Exists(FileAtoZ.CombinePaths(attachDir, emailAttach.ActualFileName)))
                {
                    //Display name is tacked onto actual file name last as to ensure file extensions are the same.
                    emailAttach.ActualFileName = FileAtoZ.CombinePaths(subDir,
                                                                       DateTime.Now.ToString("yyyyMMdd") + "_" + DateTime.Now.TimeOfDay.Ticks.ToString()
                                                                       + "_" + MiscUtils.CreateRandomAlphaNumericString(4) + "_" + emailAttach.DisplayedFileName);
                }
            }
            else
            {
                //The caller wants a specific actualFileName.  Use the given name as is.
                emailAttach.ActualFileName = FileAtoZ.CombinePaths(subDir, actualFileName);
            }
            string attachFilePath = FileAtoZ.CombinePaths(attachDir, emailAttach.ActualFileName);

            if (FileAtoZ.Exists(attachFilePath))
            {
                throw new ApplicationException("Email attachment could not be saved because a file with the same name already exists.");
            }
            try {
                FileAtoZ.WriteAllBytes(attachFilePath, arrayData);
            }
            catch (Exception ex) {
                try {
                    if (FileAtoZ.Exists(attachFilePath))
                    {
                        FileAtoZ.Delete(attachFilePath);
                    }
                }
                catch {
                    //We tried our best to delete the file, and there is nothing else to try.
                }
                throw ex;                //Show the initial error message, even if the Delete() failed.
            }
            return(emailAttach);
        }
예제 #5
0
        ///<summary>Surround with try/catch.  Also aggregates the content into the master page (unless specified to not).
        ///If isPreviewOnly, then the internal links will not be checked to see if the page exists, as it would make the refresh sluggish.
        ///And isPreviewOnly also changes the pointer so that the page looks non-clickable.
        ///For emails, this only gets called while in the email edit window. The returned string will be used to switch between plain and html text.
        ///</summary>
        public static string TranslateToXhtml(string markupText, bool isPreviewOnly, bool hasWikiPageTitles = false, bool isEmail = false, bool canAggregate = true)
        {
            //No need to check RemotingRole; no call to db.
            #region Basic Xml Validation
            string          s = markupText;
            MatchCollection matches;
            //"<",">", and "&"-----------------------------------------------------------------------------------------------------------
            s = s.Replace("&", "&amp;");
            s = s.Replace("&amp;<", "&lt;");         //because "&" was changed to "&amp;" in the line above.
            s = s.Replace("&amp;>", "&gt;");         //because "&" was changed to "&amp;" in the line above.
            s = "<body>" + s + "</body>";
            XmlDocument doc = new XmlDocument();
            using (StringReader reader = new StringReader(s)) {
                doc.Load(reader);
            }
            #endregion
            #region regex replacements
            if (isEmail)
            {
                s = TranslateEmailImages(s);              //handle email images and wiki images separately.
            }
            else
            {
                //[[img:myimage.gif]]------------------------------------------------------------------------------------------------------------
                matches = Regex.Matches(s, _odWikiImage);
                foreach (Match match in matches)
                {
                    string imgName  = match.Value.Substring(match.Value.IndexOf(":") + 1).TrimEnd("]".ToCharArray());
                    string wikiPath = "";
                    try {
                        wikiPath = WikiPages.GetWikiPath();
                    }
                    catch (Exception ex) {
                        ex.DoNothing();
                        throw;
                    }
                    string fullPath = FileAtoZ.CombinePaths(wikiPath, POut.String(imgName));
                    if (CloudStorage.IsCloudStorage)
                    {
                        //WebBrowser needs to have a local file to open, so we download the images to temp files.
                        OpenDentalCloud.Core.TaskStateDownload state = CloudStorage.Download(Path.GetDirectoryName(fullPath), Path.GetFileName(fullPath));
                        string tempFile = PrefC.GetRandomTempFile(Path.GetExtension(fullPath));
                        File.WriteAllBytes(tempFile, state.FileContent);
                        fullPath = tempFile;
                    }
                    s = s.Replace(match.Value, "<img src=\"file:///" + fullPath.Replace("\\", "/") + "\"></img>");
                }
                //[[keywords: key1, key2, etc.]]------------------------------------------------------------------------------------------------
                matches = Regex.Matches(s, _odWikiKeyword);
                foreach (Match match in matches)                 //should be only one
                {
                    s = s.Replace(match.Value, "<span class=\"keywords\">keywords:" + match.Value.Substring(11).TrimEnd("]".ToCharArray()) + "</span>");
                }
                //[[file:C:\eaula.txt]]------------------------------------------------------------------------------------------------
                matches = Regex.Matches(s, _odWikiFile);
                foreach (Match match in matches)
                {
                    string fileName = match.Value.Replace("[[file:", "").TrimEnd(']');
                    s = s.Replace(match.Value, "<a href=\"wikifile:" + fileName + "\">file:" + fileName + "</a>");
                }
                //[[folder:\\serverfiles\storage\]]------------------------------------------------------------------------------------------------
                matches = Regex.Matches(s, _odWikiFolder);
                foreach (Match match in matches)
                {
                    string folderName = match.Value.Replace("[[folder:", "").TrimEnd(']');
                    s = s.Replace(match.Value, "<a href=\"folder:" + folderName + "\">folder:" + folderName + "</a>");
                }
                //[[filecloud:AtoZ/SheetImages/happyclown.jpg]]------------------------------------------------------------------------------------------------
                matches = Regex.Matches(s, _odWikiFilecloud);
                foreach (Match match in matches)
                {
                    string fileName = CloudStorage.PathTidy(match.Value.Replace("[[filecloud:", "").TrimEnd(']'));
                    s = s.Replace(match.Value, "<a href=\"wikifilecloud:" + fileName + "\">filecloud:" + fileName + "</a>");
                }
                //[[foldercloud:AtoZ/PenguinPictures/]]------------------------------------------------------------------------------------------------
                matches = Regex.Matches(s, _odWikiFoldercloud);
                foreach (Match match in matches)
                {
                    string folderName = CloudStorage.PathTidy(match.Value.Replace("[[foldercloud:", "").TrimEnd(']'));
                    s = s.Replace(match.Value, "<a href=\"foldercloud:" + folderName + "\">foldercloud:" + folderName + "</a>");
                }
            }
            //Color and text are for both wiki and email. It's important we do this before Internal Link or else the translation may not work.
            //[[color:red|text]]----------------------------------------------------------------------------------------------------------------
            matches = Regex.Matches(s, _odWikiColor);           //.*? matches as few as possible.
            foreach (Match match in matches)
            {
                //string[] paragraphs = match.Value.Split(new string[] { "\n" },StringSplitOptions.None);
                string   tempText = "<span style=\"color:";
                string[] tokens   = match.Value.Split('|');
                if (tokens.Length < 2)               //not enough tokens
                {
                    continue;
                }
                if (tokens[0].Split(':').Length != 2)               //Must have a color token and a color value seperated by a colon, no more no less.
                {
                    continue;
                }
                for (int i = 0; i < tokens.Length; i++)
                {
                    if (i == 0)
                    {
                        tempText += tokens[0].Split(':')[1] + ";\">";                    //close <span> tag
                        continue;
                    }
                    tempText += (i > 1?"|":"") + tokens[i];
                }
                tempText  = tempText.TrimEnd(']');
                tempText += "</span>";
                s         = s.Replace(match.Value, tempText);
            }
            //[[font-family:courier|text]]----------------------------------------------------------------------------------------------------------------
            matches = Regex.Matches(s, _odWikiFont);           //.*? matches as few as possible.
            foreach (Match match in matches)
            {
                //string[] paragraphs = match.Value.Split(new string[] { "\n" },StringSplitOptions.None);
                string   tempText = "<span style=\"font-family:";
                string[] tokens   = match.Value.Split('|');
                if (tokens.Length < 2)               //not enough tokens
                {
                    continue;
                }
                if (tokens[0].Split(':').Length != 2)               //Must have a color token and a color value seperated by a colon, no more no less.
                {
                    continue;
                }
                for (int i = 0; i < tokens.Length; i++)
                {
                    if (i == 0)
                    {
                        tempText += tokens[0].Split(':')[1] + ";\">";                    //close <span> tag
                        continue;
                    }
                    tempText += (i > 1?"|":"") + tokens[i];
                }
                tempText  = tempText.TrimEnd(']');
                tempText += "</span>";
                s         = s.Replace(match.Value, tempText);
            }
            if (!isEmail)
            {
                //[[InternalLink]]--------------------------------------------------------------------------------------------------------------
                matches = Regex.Matches(s, @"\[\[.+?\]\]");
                List <string> pageNamesToCheck = new List <string>();
                List <bool>   pageNamesExist   = new List <bool>();
                string        styleNotExists   = "";
                if (hasWikiPageTitles)
                {
                    if (!isPreviewOnly)
                    {
                        foreach (Match match in matches)
                        {
                            //The '&' was replaced with '&amp;' above, so we change it back before looking for a wiki page with that name.
                            pageNamesToCheck.Add(match.Value.Trim('[', ']').Replace("&amp;", "&"));
                        }
                        if (pageNamesToCheck.Count > 0)
                        {
                            pageNamesExist = WikiPages.CheckPageNamesExist(pageNamesToCheck);                          //this gets a list of bools for all pagenames in one shot.  One query.
                        }
                    }
                    foreach (Match match in matches)
                    {
                        styleNotExists = "";
                        if (!isPreviewOnly)
                        {
                            //The '&' was replaced with '&amp;' above, so we change it back before looking for a wiki page with that name.
                            string pageName = match.Value.Trim('[', ']').Replace("&amp;", "&");
                            int    idx      = pageNamesToCheck.IndexOf(pageName);
                            if (!pageNamesExist[idx])
                            {
                                styleNotExists = "class='PageNotExists' ";
                            }
                        }
                        s = s.Replace(match.Value, "<a " + styleNotExists + "href=\"" + "wiki:" + match.Value.Trim('[', ']')            /*.Replace(" ","_")*/
                                      + "\">" + match.Value.Trim('[', ']') + "</a>");
                    }
                }
                else
                {
                    List <long>     listWikiPageNums = WikiPages.GetWikiPageNumsFromPageContent(s);
                    List <WikiPage> listWikiPages    = WikiPages.GetWikiPages(listWikiPageNums);
                    int             numInvalid       = 1;
                    foreach (Match match in matches)
                    {
                        WikiPage wp = listWikiPages.FirstOrDefault(x => x.WikiPageNum == PIn.Long(match.Value.TrimStart('[').TrimEnd(']')));
                        string   pageName;
                        if (wp != null)
                        {
                            pageName = wp.PageTitle;
                        }
                        else
                        {
                            pageName = "INVALID WIKIPAGE LINK " + numInvalid++;
                        }
                        if (!isPreviewOnly)
                        {
                            styleNotExists = "";
                            if (wp == null)
                            {
                                styleNotExists = "class='PageNotExists' ";
                            }
                        }
                        pageName = pageName.Replace("&", "&amp;").Replace("&amp;<", "&lt;").Replace("&amp;>", "&gt;");
                        string replace = "<a " + styleNotExists + "href=\"" + "wiki:" + pageName /*.Replace(" ","_")*/ + "\">" + pageName + "</a>";
                        Regex  regex   = new Regex(Regex.Escape(match.Value));
                        //Replace the first instance of the match with the wiki page name (or unknown if not found).
                        s = regex.Replace(s, replace, 1);
                    }
                }
            }
            //Unordered List----------------------------------------------------------------------------------------------------------------
            //Instead of using a regex, this will hunt through the rows in sequence.
            //later nesting by running ***, then **, then *
            s = ProcessList(s, "*");
            //numbered list---------------------------------------------------------------------------------------------------------------------
            s = ProcessList(s, "#");
            //table-------------------------------------------------------------------------------------------------------------------------
            //{|
            //!Width="100"|Column Heading 1!!Width="150"|Column Heading 2!!Width=""|Column Heading 3
            //|-
            //|Cell 1||Cell 2||Cell 3
            //|-
            //|Cell A||Cell B||Cell C
            //|}
            //There are many ways to parse this.  Our strategy is to do it in a way that the generated xml is never invalid.
            //As the user types, the above example will frequently be in a state of partial completeness, and the parsing should gracefully continue anyway.
            //rigorous enforcement only happens when validating during a save, not here.
            matches = Regex.Matches(s, _odWikiTable, RegexOptions.Singleline);
            foreach (Match match in matches)
            {
                //If there isn't a new line before the start of the table markup or after the end, the match group value will be an empty string
                //Tables must start with "'newline'{|" and end with "|}'newline'"
                string        tableStrOrig = match.Value;
                StringBuilder strbTable    = new StringBuilder();
                string[]      lines        = tableStrOrig.Split(new string[] { "{|\n", "\n|-\n", "\n|}" }, StringSplitOptions.RemoveEmptyEntries);
                strbTable.AppendLine("<table>");
                List <string> colWidths = new List <string>();
                for (int i = 0; i < lines.Length; i++)
                {
                    if (lines[i].StartsWith("!"))                     //header
                    {
                        strbTable.AppendLine("<tr>");
                        lines[i] = lines[i].Substring(1);                      //strips off the leading !
                        string[] cells = lines[i].Split(new string[] { "!!" }, StringSplitOptions.None);
                        colWidths.Clear();
                        for (int c = 0; c < cells.Length; c++)
                        {
                            if (Regex.IsMatch(cells[c], @"(Width="")\d+""\|"))                           //e.g. Width="90"|
                            {
                                strbTable.Append("<th ");
                                string width = cells[c].Substring(7);                              //90"|Column Heading 1
                                width = width.Substring(0, width.IndexOf("\""));                   //90
                                colWidths.Add(width);
                                strbTable.Append("Width=\"" + width + "\">");
                                strbTable.Append(ProcessParagraph(cells[c].Substring(cells[c].IndexOf("|") + 1), false));                             //surround with p tags. Allow CR in header.
                                strbTable.AppendLine("</th>");
                            }
                            else
                            {
                                strbTable.Append("<th>");
                                strbTable.Append(ProcessParagraph(cells[c], false));                               //surround with p tags. Allow CR in header.
                                strbTable.AppendLine("</th>");
                            }
                        }
                        strbTable.AppendLine("</tr>");
                    }
                    else if (lines[i].Trim() == "|-")
                    {
                        //totally ignore these rows
                    }
                    else                     //normal row
                    {
                        strbTable.AppendLine("<tr>");
                        lines[i] = lines[i].Substring(1);                      //strips off the leading |
                        string[] cells = lines[i].Split(new string[] { "||" }, StringSplitOptions.None);
                        for (int c = 0; c < cells.Length; c++)
                        {
                            strbTable.Append("<td Width=\"" + colWidths[c] + "\">");
                            strbTable.Append(ProcessParagraph(cells[c], false));
                            strbTable.AppendLine("</td>");
                        }
                        strbTable.AppendLine("</tr>");
                    }
                }
                strbTable.Append("</table>");
                s = s.Replace(tableStrOrig, strbTable.ToString());
            }
            #endregion regex replacements
            #region paragraph grouping
            StringBuilder strbSnew = new StringBuilder();
            //a paragraph is defined as all text between sibling tags, even if just a \n.
            int iScanInParagraph = 0;          //scan starting at the beginning of s.  S gets chopped from the start each time we grab a paragraph or a sibiling element.
            //The scanning position represents the verified paragraph content, and does not advance beyond that.
            //move <body> tag over.
            strbSnew.Append("<body>");
            s = s.Substring(6);
            bool startsWithCR = false;          //todo: handle one leading CR if there is no text preceding it.
            if (s.StartsWith("\n"))
            {
                startsWithCR = true;
            }
            string tagName;
            Match  tagCurMatch;
            while (true)                                             //loop to either construct a paragraph, or to immediately add the next tag to strbSnew.
            {
                iScanInParagraph = s.IndexOf("<", iScanInParagraph); //Advance the scanner to the start of the next tag
                if (iScanInParagraph == -1)                          //there aren't any more tags, so current paragraph goes to end of string.  This won't happen
                {
                    throw new ApplicationException(Lans.g("WikiPages", "No tags found."));
                    //strbSnew.Append(ProcessParagraph(s));
                }
                if (s.Substring(iScanInParagraph).StartsWith("</body>"))
                {
                    strbSnew.Append(ProcessParagraph(s.Substring(0, iScanInParagraph), startsWithCR));
                    //startsWithCR=false;
                    //strbSnew.Append("</body>");
                    s = "";
                    iScanInParagraph = 0;
                    break;
                }
                tagName     = "";
                tagCurMatch = Regex.Match(s.Substring(iScanInParagraph), "^<.*?>");             //regMatch);//.*? means any char, zero or more, as few as possible
                if (tagCurMatch == null)
                {
                    //shouldn't happen unless closing bracket is missing
                    throw new ApplicationException(Lans.g("WikiPages", "Unexpected tag:") + " " + s.Substring(iScanInParagraph));
                }
                if (tagCurMatch.Value.Trim('<', '>').EndsWith("/"))
                {
                    //self terminating tags NOT are allowed
                    //this should catch all non-allowed self-terminating tags i.e. <br />, <inherits />, etc...
                    throw new ApplicationException(Lans.g("WikiPages", "All elements must have a beginning and ending tag. Unexpected tag:") + " " + s.Substring(iScanInParagraph));
                }
                //Nesting of identical tags causes problems:
                //<h1><h1>some text</h1></h1>
                //The first <h1> will match with the first </h1>.
                //We don't have time to support this outlier, so we will catch it in the validator when they save.
                //One possible strategy here might be:
                //idxNestedDuplicate=s.IndexOf("<"+tagName+">");
                //if(idxNestedDuplicate<s.IndexOf("</"+tagName+">"){
                //
                //}
                //Another possible strategy might be to use regular expressions.
                tagName = tagCurMatch.Value.Split(new string[] { "<", " ", ">" }, StringSplitOptions.RemoveEmptyEntries)[0]; //works with tags like <i>, <span ...>, and <img .../>
                if (s.IndexOf("</" + tagName + ">") == -1)                                                                   //this will happen if no ending tag.
                {
                    throw new ApplicationException(Lans.g("WikiPages", "No ending tag:") + " " + s.Substring(iScanInParagraph));
                }
                switch (tagName)
                {
                case "a":
                case "b":
                case "div":
                case "i":
                case "span":
                    iScanInParagraph = s.IndexOf("</" + tagName + ">", iScanInParagraph) + 3 + tagName.Length;
                    continue;                            //continues scanning this paragraph.

                case "h1":
                case "h2":
                case "h3":
                case "ol":
                case "ul":
                case "table":
                case "img":                        //can NOT be self-terminating
                    if (iScanInParagraph == 0)     //s starts with a non-paragraph tag, so there is no partially assembled paragraph to process.
                    //do nothing
                    {
                    }
                    else                              //we are already part way into assembling a paragraph.
                    {
                        strbSnew.Append(ProcessParagraph(s.Substring(0, iScanInParagraph), startsWithCR));
                        startsWithCR     = false;                          //subsequent paragraphs will not need this
                        s                = s.Substring(iScanInParagraph);  //chop off start of s
                        iScanInParagraph = 0;
                    }
                    //scan to the end of this element
                    int iScanSibling = s.IndexOf("</" + tagName + ">") + 3 + tagName.Length;
                    //tags without a closing tag were caught above.
                    //move the non-paragraph content over to s new.
                    strbSnew.Append(s.Substring(0, iScanSibling));
                    s = s.Substring(iScanSibling);
                    //scanning will start a totally new paragraph
                    break;

                default:
                    if (isEmail)
                    {
                        iScanInParagraph = s.IndexOf("</" + tagName + ">", iScanInParagraph) + 3 + tagName.Length;
                        continue;                                //continues scanning this paragraph
                    }
                    throw new ApplicationException(Lans.g("WikiPages", "Unexpected tag:") + " " + s.Substring(iScanInParagraph));
                }
            }
            strbSnew.Append("</body>");
            #endregion
            #region aggregation
            doc = new XmlDocument();
            using (StringReader reader = new StringReader(strbSnew.ToString())) {
                doc.Load(reader);
            }
            StringBuilder     strbOut  = new StringBuilder();
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent             = true;
            settings.IndentChars        = "\t";
            settings.OmitXmlDeclaration = true;
            settings.NewLineChars       = "\n";
            using (XmlWriter writer = XmlWriter.Create(strbOut, settings)) {
                doc.WriteTo(writer);
            }
            //spaces can't be handled prior to this point because &nbsp; crashes the xml parser.
            strbOut.Replace("  ", "&nbsp;&nbsp;");           //handle extra spaces.
            strbOut.Replace("<td></td>", "<td>&nbsp;</td>"); //force blank table cells to show not collapsed
            strbOut.Replace("<th></th>", "<th>&nbsp;</th>"); //and blank table headers
            strbOut.Replace("{{nbsp}}", "&nbsp;");           //couldn't add the &nbsp; earlier because
            strbOut.Replace("<p></p>", "<p>&nbsp;</p>");     //probably redundant but harmless
            //aggregate with master
            if (isEmail)
            {
                if (canAggregate)
                {
                    s = PrefC.GetString(PrefName.EmailMasterTemplate).Replace("@@@body@@@", strbOut.ToString());
                    return(s);
                }
                return(strbOut.ToString());
            }
            else
            {
                s = WikiPages.MasterPage.PageContent.Replace("@@@body@@@", strbOut.ToString());
            }
            #endregion aggregation

            /*
             * //js This code is buggy.  It will need very detailed comments and careful review before/if we ever turn it back on.
             * if(isPreviewOnly) {
             *      //do not change cursor from pointer to IBeam to Hand as you move the cursor around the preview page
             *      s=s.Replace("*{\n\t","*{\n\tcursor:default;\n\t");
             *      //do not underline links if you hover over them in the preview window
             *      s=s.Replace("a:hover{\n\ttext-decoration:underline;","a:hover{\n\t");
             * }*/
            return(s);
        }