public static void ParseLevels(string html, Spell spell, ILog log)
        {
            List<SpellListLevel> levels = new List<SpellListLevel>();

            ParseClassLevel(html, levels, SpellList.Ids.Bard, string.Format(ClassPattern, "barde", "Bard"));
            if (!ParseClassLevel(html, levels, SpellList.Ids.SorcererWizard, EnsWizPattern1))
            {
                // Version alternative
                if (!ParseClassLevel(html, levels, SpellList.Ids.SorcererWizard, string.Format(ClassPattern, "magicien", "Mag")))
                {
                    ParseClassLevel(html, levels, SpellList.Ids.SorcererWizard, EnsWizPattern2);
                }
            }

            ParseClassLevel(html, levels, SpellList.Ids.Ranger, string.Format(ClassPattern, "rôdeur", "Rôd"));
            ParseClassLevel(html, levels, SpellList.Ids.Paladin, string.Format(ClassPattern, "paladin", "Pal"));
            ParseClassLevel(html, levels, SpellList.Ids.Druid, string.Format(ClassPattern, "druide", "Dru"));
            ParseClassLevel(html, levels, SpellList.Ids.Cleric, string.Format(ClassPattern, "prêtre", "Prê"));
            ParseClassLevel(html, levels, SpellList.Ids.Inquisitor, string.Format(ClassPattern, "inquisiteur", "Inq"));
            ParseClassLevel(html, levels, SpellList.Ids.Summoner, string.Format(ClassPattern, "invocateur|conjurateur", "Inv"));
            ParseClassLevel(html, levels, SpellList.Ids.Witch, string.Format(ClassPattern, "sorcière", "Sor"));
            ParseClassLevel(html, levels, SpellList.Ids.Alchemist, string.Format(ClassPattern, "alchimiste", "Alch"));
            ParseClassLevel(html, levels, SpellList.Ids.Oracle, string.Format(ClassPattern, "oracle", "Ora"));
            ParseClassLevel(html, levels, SpellList.Ids.Oracle, string.Format(ClassPattern, "prêtre", "Prê"));
            ParseClassLevel(html, levels, SpellList.Ids.Magus, string.Format(ClassPattern, "magus", "magus"));
            ParseClassLevel(html, levels, SpellList.Ids.AntiPaladin, string.Format(ClassPattern, "antipaladin", "antipaladin"));

            if (levels.Count == 0)
            {
                throw new ParseException("Impossible de détecter les niveaux de classe");
            }

            spell.Levels = levels.ToArray();
        }
        public static void ParseCastingTime(string html, Spell spell, ILog log)
        {
            var match = Regex.Match(html, Pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);

            if (!match.Success)
            {
                throw new ParseException("Impossible de détecter le temps d'incantation");
            }

            var castingTime = spell.CastingTime = new SpellCastingTime();

            var value = match.Groups["Value"].Value.Trim();

            match = Regex.Match(value, ValuePattern, RegexOptions.CultureInvariant);

            if (match.Success)
            {
                castingTime.Value = int.Parse(match.Groups["Quantity"].Value);

                var unit = match.Groups["Unit"].Value.ToLowerInvariant();

                if (unit.IndexOf("voir description") != -1)
                    castingTime.Unit = TimeUnit.Special;
                else
                    castingTime.Unit = ParseTimeUnit(unit);
            }

            if (!match.Success || castingTime.Unit == TimeUnit.Special)
            {
                castingTime.Value = 0;
                castingTime.Unit = TimeUnit.Special;
                castingTime.Text = MarkupUtil.RemoveMarkup(value);
            }
        }
        public static void ParseTarget(string html, Spell spell, ILog log)
        {
            var match = Regex.Match(html, Pattern, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);

            if (!match.Success)
            {
                if (!noTargetSpells.Contains(spell.Id))
                {
                    log.Warning($"{spell.Name} : cible introuvable. Si le sort n'a pas de cible, il doit être ajouté dans le fichier contenant les sorts sans cible");
                }

                return;
            }

            var type = match.Groups["Type"].Value;
            int count;
            typeCount.TryGetValue(type, out count);
            count++;
            typeCount[type] = count;

            var value = MarkupUtil.RemoveMarkup(match.Groups["Value"].Value.Trim());

            spell.Target = new SpellTarget
            {
                Value = value
            };
        }
 public static bool TryParse(WikiPage page, out Spell spell, ILog log = null)
 {
     spell = null;
     try
     {
         spell = Parse(page, log);
         return true;
     }
     catch (ParseException e)
     {
         log.Error("{0}: {1}", page.Title, e.Message);
         return false;
     }
 }
        public static void ParseDescriptor(string html, Spell spell, ILog log)
        {
            var match = Regex.Match(html, DescriptorPattern, RegexOptions.CultureInvariant);

            if (!match.Success)
            {
                match = Regex.Match(html, DescriptorPattern2, RegexOptions.CultureInvariant);
            }

            if (match.Success)
            {
                foreach (var descriptor in match.Groups["Title"].Value.Split(',').Select(d => d.Trim().ToLowerInvariant()))
                {
                    spell.Descriptor |= ParseDescriptorValue(descriptor);
                }
            }
        }
        public static void ParseSchool(string html, Spell spell, ILog log)
        {
            var schoolMatch = Regex.Match(html, SchoolPattern, RegexOptions.CultureInvariant);

            if (schoolMatch.Success)
            {
                spell.School = ParseSpellSchoolTitle(schoolMatch.Groups["Title"].Value);
            }
            else if (html.IndexOf(UniversalSchoolPattern, StringComparison.OrdinalIgnoreCase) != -1)
            {
                spell.School = SpellSchool.Universal;
            }
            else
            {
                throw new ParseException("Impossible de lire l'école de magie");
            }

            // TODO lire les branches
        }
        private static Spell Parse(WikiPage page, ILog log = null)
        {
            var spell = new Spell();
            spell.Name = page.Title;
            spell.Id = page.Id;

            // Ajout source wiki
            spell.Source.References.Add(References.FromFrWiki(page.Url));

            // Ajout source drp
            spell.Source.References.Add(References.FromFrDrp(page.Id));

            var wiki = page.Raw;

            if (wiki.IndexOf("'''École'''", StringComparison.Ordinal) == -1 && wiki.IndexOf("'''Ecole'''", StringComparison.Ordinal) == -1)
            {
                throw new ParseException("École de magie introuvable");
            }

            SpellParser parser = new SpellParser(spell, wiki);
            parser.Execute(log);

            return spell;
        }
 public SpellParser(Spell spell, string html)
 {
     this.spell = spell;
     this.html = html;
 }
        public static void ParseSavingThrow(string html, Spell spell, ILog log)
        {
            if (spell.Name == "Transmutation de potion en poison")
            {
                // Cas particulier : ce sort décrit plusieurs sorts
                return;
            }

            var match = Regex.Match(html, Pattern, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);

            if (!match.Success)
            {
                return;
                throw new ParseException("Jet de sauvegarde introuvable");
            }

            var value = MarkupUtil.RemoveMarkup(match.Groups["Value"].Value.Trim());
            var compareValue = value.ToLowerInvariant();

            var savingThrow = new SpellSavingThrow();

            var copyValue = false;

            if (compareValue.IndexOf(" ou ", StringComparison.Ordinal) != -1 ||
                compareValue.IndexOf(" et ", StringComparison.Ordinal) != -1 ||
                compareValue.IndexOf(" puis ", StringComparison.Ordinal) != -1)
            {
                copyValue = true;
                savingThrow.Target = SpellSavingThrowTarget.Special;
                savingThrow.Effect = SpellSavingThrowEffect.Special;
            }
            else if ((match = Regex.Match(compareValue, EffectPattern, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture)).Success)
            {
                switch (match.Groups["Type"].Value)
                {
                    case "volonté":
                        savingThrow.Target = SpellSavingThrowTarget.Will;
                        break;

                    case "réflexes":
                        savingThrow.Target = SpellSavingThrowTarget.Reflex;
                        break;

                    case "vigueur":
                        savingThrow.Target = SpellSavingThrowTarget.Fortitude;
                        break;

                    case "oui":
                        savingThrow.Target = SpellSavingThrowTarget.Special;
                        copyValue = true;
                        break;

                    case "non":
                        savingThrow.Target = SpellSavingThrowTarget.None;
                        copyValue = true;
                        break;
                }

                var effect = match.Groups["Value"].Value.Trim();

                if (effect.EndsWith(", voir texte"))
                {
                    copyValue = true;
                    effect = effect.Substring(0, effect.Length - ", voir texte".Length);
                }
                else if (effect.EndsWith(", voir description"))
                {
                    copyValue = true;
                    effect = effect.Substring(0, effect.Length - ", voir description".Length);
                }
                else if (effect.EndsWith(", voir plus bas"))
                {
                    copyValue = true;
                    effect = effect.Substring(0, effect.Length - ", voir plus bas".Length);
                }

                match = Regex.Match(effect, BonusPattern, RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);

                if (match.Success)
                {
                    effect = match.Groups["Effect"].Value.Trim();

                    foreach (Capture bonusCapture in match.Groups["Parenthesis"].Captures)
                    {
                        var bonus = bonusCapture.Value.Trim().ToLowerInvariant();

                        switch (bonus)
                        {
                            case "inoffensif":
                                savingThrow.Harmless = true;
                                break;

                            case "objet":
                                savingThrow.Objects = true;
                                break;

                            case "inoffensif, objet":
                            case "objet, inoffensif":
                                savingThrow.Harmless = true;
                                savingThrow.Objects = true;
                                break;

                            case "spécial, voir texte":
                            case "voir texte":
                            case "voir plus bas":
                            case "voir description":
                            case "en cas d’interaction":
                            case "en cas d'interaction":
                            case "spécial, voir plus bas":
                                copyValue = true;
                                break;

                            case "":
                                break;

                            default:
                                throw new ParseException(string.Format("Détail du jet de sauvegarde non reconnu : \"{0}\" dans le texte \"{1}\"", bonus, value));
                        }
                    }
                }

                switch (effect)
                {
                    case "annuler":
                    case "annule":
                        savingThrow.Effect = SpellSavingThrowEffect.Negates;
                        break;

                    case "1/2 dégâts":
                    case "réduit de moitié":
                    case "réduire de moitié":
                        savingThrow.Effect = SpellSavingThrowEffect.Half;
                        break;

                    case "partiel":
                    case "partielle":
                        savingThrow.Effect = SpellSavingThrowEffect.Partial;
                        break;

                    case "dévoile":
                    case "dévoiler":
                    case "dévoiler l'illusion":
                        savingThrow.Effect = SpellSavingThrowEffect.Disbelief;
                        break;

                    case "":
                        break;

                    default:
                        if (copyValue)
                        {
                            // On a déjà détecté un sort spécial, on arrête là
                        }
                        else
                        {
                            throw new ParseException(string.Format("Impossible de décoder l'effet du jet de sauvegarde \"{0}\"", effect));
                        }
                        break;
                }
            }
            else if (compareValue.EqualsOrdinal("aucun") || compareValue.EqualsOrdinal("non"))
            {
                savingThrow = null;
            }
            else if (compareValue.StartsWithOrdinal("aucun"))
            {
                savingThrow.SpecificValue = value;
            }
            else if (compareValue.EqualsOrdinal("voir description") || compareValue.EqualsOrdinal("voir texte"))
            {
                savingThrow.Effect = SpellSavingThrowEffect.Special;
                savingThrow.SpecificValue = value;
            }
            else if (compareValue.EqualsOrdinal("spécial, voir plus bas"))
            {
                savingThrow.Effect = SpellSavingThrowEffect.Special;
                savingThrow.SpecificValue = value;
            }
            else if (compareValue.EqualsOrdinal("spécial, voir texte"))
            {
                savingThrow.Effect = SpellSavingThrowEffect.Special;
                savingThrow.SpecificValue = value;
            }
            else
            {
                throw new ParseException(string.Format("Impossible de décoder le type de jet de sauvegarde dans \"{0}\"", value));
            }

            if (copyValue)
            {
                savingThrow.SpecificValue = value;
            }

            spell.SavingThrow = savingThrow;
        }
 public SpellWrapper(Spell spell)
 {
     this.spell = spell;
 }
        public static void ParseRange(string html, Spell spell, ILog log)
        {
            var match = Regex.Match(html, RangePattern, RegexOptions.CultureInvariant);

            if (!match.Success)
            {
                if (!noRangeSpells.Contains(spell.Id))
                {
                    throw new ParseException($"Portée introuvable pour l'id {spell.Id}");
                }
                // portée ignorée
                return;
            }

            var rangeValue = match.Groups["Value"].Value.Trim();

            switch (rangeValue.ToLowerInvariant())
            {
                case "[[personnelle]]":
                case "personnelle":
                case "[[présentation des sorts#portee|personnelle]]":
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Personal};
                    break;

                case "[[contact]]":
                case "contact":
                case "[[présentation des sorts#portee|contact]]":
                case "[[présentation des sorts#contact|contact]]":
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Touch};
                    break;

                case "[[illimitée]]":
                case "illimitée":
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Unlimited};
                    break;

                case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]])":
                case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) (5 cases + 1 case/2 [[niveau|niveaux]])": // Format sans snippet
                case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Format APG
                case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]])/(5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Format UM
                case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) / (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Format UM 2
                case "courte (7,50 m + 1,50 m/2 [niveau|niveaux]])/(5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Format UM (buggé!)
                case "courte (7,5 m + 1,5 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Format court (pas de décimale non significative)
                case "courte (7,50 m + 1,50 m/2 niveaux) (5 cases + 1 case/2 niveaux)":
                case "ccourte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Transmutation du sang en acide (BUG)
                    //case "courte (7,50 m + 1,50 m/2 ([niveau|niveaux]])": // Détection faussée
                    //case "courte (7,50 mètres + 1,50 mètre/2 [[niveau|niveaux]])": // Convocation de monstres I
                    //case "proche (7,50 m + 1,50 m/2 [[niveau|niveaux]])": // Délivrance de la paralysie
                    //case "courte (7,50 m/5 cases + 1,50 m/1 case par 2 [[niveau|niveaux]])": // Illumination
                    //case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]])/(5 cases + 1 case/2 [[niveau|niveaux]])": // Lenteur
                    //case "courte (7,50 m + 1,50 m/2 niveaux) / (5 cases + 1 case/2 [[niveau|niveaux]])": // Lien télépathique
                    //case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) / (5 cases + 1 case/2 [[niveau|niveaux]])": // Manipulation à distance
                    //case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) / (5 cases + 1 cases/2 [[niveau|niveaux]])": // Mauvais oeil
                    //case "courte (7,50 + 1,50 m/2 [[niveau|niveaux]])": // Profanation
                    //case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 ({s:c}))": // Vermine géante
                    //case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]] - 5 cases + 1 case/2 [[niveau|niveaux]])": // Zone de vérité
                    //case "courte (7,50 m + 1,50 m/2 niveaux) (5 {s:c} + 1 {s:c}/2 niveaux)": // Appel cacophonique
                    //case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux])": // Bouclier involontaire
                    //case "courte (7,5 m + 1,5 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Dangereux final
                    //case "courte (7,50 m + 1,50 m/2 niveaux)": // Négation de l'arôme
                    //case "courte (7,50 m + 1,50 m/2 niveaux) (5 cases + 1 case/2 niveaux)": // Vérole
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Close};
                    break;

                // Format standard
                case "moyenne (30 m + 3 m/[[niveau]])":
                case "moyenne (30 m + 3 m/[[niveau]]) (20 {s:c} + 2 {s:c}/[[niveau]])":
                case "moyenne (30 m + 3 m/[[niveau|niveaux]])/(20 {s:c} + 2 {s:c}/[[niveau|niveaux]])":
                case "moyenne (30 m + 3 m/[[niveau]]) / (20 {s:c} + 2 {s:c}/[[niveau]])":
                //case "moyenne (20 cases + 2 cases/[[niveau]]) (30 m + 3 m/[[niveau]])": // Extinction des feux
                //case "moyenne (30 m/20 cases + 3 m/2 cases par [[niveau]])": // Immobilisation de morts-vivants
                //case "moyenne (30 m + 3 m/[[niveau]]) / (20 cases + 2 cases/[[niveau]])": // Lueur d'arc-en-ciel
                //case "moyenne (30 m + 3 m/[[niveau]])/(20 cases + 2 cases/[[niveau]])": // Main spectrale
                //case "moyenne (30 m + 3 m/[[niveau]]) / (20 cases + 2 case/[[niveau]])": // Mur de feu
                //case "moyenne (30 m + 3 m/niveau) / (20 cases + 2 cases/[[niveau]])": // Mur de glace
                //case "moyenne (30 m + 3 m/niveau)": // Pétrification
                // Format APG
                case "intermédiaire (30 m + 3 m/[[niveau]]) (20 {s:c} + 2 {s:c}/[[niveau]])":
                    //case "intermédiaire (30 m + 3 m/niveau)": // Appel des pierres
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Medium};
                    break;

                case "longue (120 m + 12 m/[[niveau]])": // Format standard
                case "longue (120 m + 12 m/[[niveau]]) (80 {s:c} + 8 {s:c}/[[niveau]])": // Format APG
                case "longue (120 m + 12 m/[[niveau]]) / (80 {s:c} + 8 {s:c}/[[niveau]])": // Format UM
                    //case "longue (120 m/80 cases + 12 m/8 cases par [[niveau]])": // Image silencieuse
                    //case "longue (120 m + 12 m/[[niveau]])/(80 cases + 8 cases/[[niveau]])": // Localisation d'objets
                    //case "longue (120 m + 12 m/[[niveau]]) / (80 cases + 8 cases/[[niveau]])": // Lueur féerique
                    //case "longue (120 m + 12 m/niveau)": // Sphère glaciale
                    //case "longue (120 m + 12 m/niveau) (80  cases + 8 cases/niveau)": // Tsunami
                    //case "longue (120 m + 12 m/niveau) (80 cases) + 8 cases par niveau)": // Vortex
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Long};
                    break;

                case "0 m":
                case "0 m (0 {s:c})":
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "0"};
                    break;

                case "1,50 m": // Charmant cadeau
                case "1,50 m (1 {s:c})": // Manteau de rêves
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "1"};
                    break;

                case "3 m":
                //case "3 m - 2 cases": // Zone d'antimagie
                case "3 m (2 {s:c})": // Interdiction du fou
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "2"};
                    break;

                case "4,50 m":
                case "4,50 m (3 {s:c})":
                    //case "4,50 m/3 cases": // Mains brûlantes
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "3"};
                    break;

                case "6 m":
                //case "6 m (4 cases)": // Manteau du chaos
                case "6 m (4 {s:c})": // Final revigorant
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "4"};
                    break;

                case "9 m":
                case "9 m (6 {s:c})": // Cri strident
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "6"};
                    break;

                case "12 m":
                case "12 m (8 {s:c})": // Recherche de pensées
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "8"};
                    break;

                case "15 m/10 cases":
                case "15 m/10 {s:c}": // Imprécation
                case "15 m (10 {s:c})": // Bénédiction
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "10"};
                    break;

                case "18 mètres":
                case "18 m":
                case "18 m/12 cases":
                case "18 m (12 {s:c})":
                case "18 mètres (12 {s:c})": // Extase
                case "18 m/12 {s:c}": // Identification
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "12"};
                    break;

                case "30 m (20 {s:c})": // Joueur de flûte
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "20"};
                    break;

                case "36 m": // Sillage de lumière
                case "36 m (24 {s:c})": // Éclair
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "24"};
                    break;

                case "jusqu’à 3 m (2 {s:c})/[[niveau]]": // Champ de force
                    spell.Range = new SpellRange {SpecificValue = "jusqu'à 2 cases/niveau"};
                    break;

                case "12 m (8 {s:c})/[[niveau]]": // Contrôle des vents
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "8/level"};
                    break;

                case "15 m (10 {s:c})/[[niveau]]": // Entrer dans une image
                    spell.Range = new SpellRange {Unit = SpellRangeUnit.Squares, SpecificValue = "10/level"};
                    break;

                case "1,5 km/[[niveau]]": // Vent des murmures
                    spell.Range = new SpellRange {SpecificValue = "1,5 km/niveau"};
                    break;

                case "3 ou 9 m (2 ou 6 {s:c})": // Détonation discordante
                    spell.Range = new SpellRange {SpecificValue = "3 ou 9 m (2 ou 6 cases)"};
                    break;

                case "9 ou 18 m (6 ou 12 {s:c})": // Souffle de dragon
                    spell.Range = new SpellRange {SpecificValue = "9 ou 18 m (6 ou 12 cases)"};
                    break;

                case "voir description": // Coffre secret
                case "courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]]) (voir description)":
                case "[[présentation des sorts#portee|contact]] (voir description)": // Passage dans l'éther
                case "0 m (voir description)": // Symbole de mort
                case "3 m (voir description)": // Communication avec les apparitions
                case "spéciale (voir description)": // Enchevetrement flamboyant & Flammes de la vengeance
                //case "contact ; voir texte": // Lien sacré
                //case "voir texte": // Vague mondiale
                case "[[présentation des sorts#portee|contact]] et [[présentation des sorts#portee|illimitée]] (voir description)": // Vengeance fantasmagorique
                case "[[personnelle]] ou 1,50 m (1 {s:c}) (voir description)": // Voile d'énergie positive & Voile du paradis
                case "3 km": // Contrôle du climat
                case "7,5 km": // Main du berger
                case "1,5 km": // Oeil indiscret
                case "n’importe où dans la zone défendue": // Défense magique
                case "[[présentation des sorts#portee|personnelle]] ou [[présentation des sorts#portee|contact]]": // Invisibilité, Prémonition et Sphère d'invisibilité
                case "[[personnelle]] ou [[contact]]":
                case "[[personnelle]] ou courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Lévitation
                case "[[personnelle]] et [[contact]]": // Téléportation et Téléportation interplanétaire
                case "[[personnelle]] et [[présentation des sorts#portee|contact]]":
                case "[[personnelle]] ou [[présentation des sorts#portee|contact]]": // Liberté de mouvement, orientation
                case "9 ou 18 m": // Souffle de dragon
                case "[[contact]] (voir description)": // *
                case "[[présentation des sorts#portee|contact]] ou 1,50 m (1 {s:c}) (voir description)": // Manteau de colère
                case "[[contact]] ou 1,50 m (1 {s:c}) (voir description)": // Manteau de colère
                case "[[personnelle]] et courte (7,50 m + 1,50 m/2 [[niveau|niveaux]]) (5 {s:c} + 1 {s:c}/2 [[niveau|niveaux]])": // Lien des esprits combatifs
                case "[[émanation]] de 9 m (6 {s:c}) de rayon centrée sur le lanceur de sorts": // Note pétrifiante
                    spell.Range = new SpellRange
                    {
                        SpecificValue = rangeValue
                            .Replace("[[niveau|niveaux]]", "niveaux")
                            .Replace("[[", string.Empty)
                            .Replace("]]", string.Empty)
                            .Replace("{s:c}", "case(s)")
                    };
                    break;

                default:
                    log.Warning("{0}: Portée non reconnue \"{1}\"", spell.Name, rangeValue);
                    spell.Range = new SpellRange
                    {
                        SpecificValue = MarkupUtil.RemoveMarkup(rangeValue)
                    };
                    break;
            }

            if (spell.Range != null)
            {
                return;
            }

            const string meterRange = "^(\\d+) m$";

            match = Regex.Match(rangeValue, meterRange, RegexOptions.CultureInvariant);

            if (match.Success)
            {
            }
        }
 private string AsSourceLabel(Spell spell)
 {
     switch (spell.Source.Id)
     {
         case Source.Ids.PathfinderRpg:
             return "PHB";
         case Source.Ids.AdvancedPlayerGuide:
             return "APG";
         case Source.Ids.UltimateMagic:
             return "UM";
         case Source.Ids.UltimateCombat:
             return "UC";
         default:
             return string.Empty;
     }
 }
        private static string GetSpellListLevel(Spell spell, string spellListId)
        {
            var level = spell.Levels.FirstOrDefault(l => l.List == spellListId);

            return level != null ? level.Level.ToString() : string.Empty;
        }
        private static string GetLocalizedEntry(Spell spell, string language, string href, string defaultValue = null)
        {
            if (spell.Localization == null)
                return defaultValue;

            return spell.Localization.GetLocalizedEntry(language, href, defaultValue);
        }
        public static void ParseComponents(string html, Spell spell, ILog log)
        {
            var match = Regex.Match(html, Pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);

            if (!match.Success)
            {
                if (!noComponentSpells.Contains(spell.Id))
                {
                    throw new ParseException($"Composantes introuvables pour l'id {spell.Id}");
                }
                // composantes ignorée
                return;
            }

            spell.Components = new SpellComponents();

            var components = match.Groups["Value"].Value.Split(',').Select(s => s.Trim().ToLowerInvariant());

            foreach (var c in components)
            {
                switch (c)
                {
                    case "m":
                        spell.Components.Kinds |= SpellComponentKinds.Material;
                        break;

                    case "v":
                        spell.Components.Kinds |= SpellComponentKinds.Verbal;
                        break;

                    case "g":
                    case "s": // Désactiver pour trouver tous les sorts de l'APG avec le bug
                        spell.Components.Kinds |= SpellComponentKinds.Somatic;
                        break;

                    case "fd":
                        spell.Components.Kinds |= SpellComponentKinds.DivineFocus;
                        break;

                    case "f":
                        spell.Components.Kinds |= SpellComponentKinds.Focus;
                        break;

                    case "m/fd":
                    case "fd/m":
                        spell.Components.Kinds |= SpellComponentKinds.MaterialOrDivineFocus;
                        break;

                    case "f/fd":
                    case "fd/f":
                        spell.Components.Kinds |= SpellComponentKinds.FocusOrDivineFocus;
                        break;

                    default:
                        throw new ParseException("Formule de composantes inconnue");
                }
            }

            var comment = match.Groups["Comment"].Value.Trim();

            if (comment.Length > 1)
            {
                if (comment[0] == '(' && comment[comment.Length - 1] == ')')
                {
                    comment = comment.Substring(1, comment.Length - 2);
                }
            }

            spell.Components.Description = MarkupUtil.RemoveMarkup(comment);
        }
        public static void ParseMagicResistance(string html, Spell spell, ILog log)
        {
            const string pattern = "'''Résistance à la magie''' ";
            var i = html.IndexOf(pattern, StringComparison.Ordinal);

            var spellResist = spell.SpellResistance = new SpellResistance();

            if (i == -1)
            {
                //throw new ParseException("Résistance à la magie introuvable");
                spellResist.Resist = SpecialBoolean.No;
                return;
            }

            var eol = html.IndexOf('\n', i);

            if (eol == -1)
            {
                throw new ParseException("Résistance à la magie mal formée");
            }

            i += pattern.Length;

            // La valeur de résistance à la magie est la fin de la phrase
            var value = html.Substring(i, eol - i);

            // On conserve la valeur originale (si la détection échoue, c'est elle que l'on indiquera)
            var originalValue = value;

            // On supprime les éventuels liens indiqués en fin de RM
            value = Regex.Replace(value.ToLowerInvariant(), RemoveMarkupPattern, "$2").Trim();

            switch (value)
            {
                case No:
                    // Il ne reste plus que le texte "non" comme valeur, on le renvoie
                    spellResist.Resist = SpecialBoolean.No;
                    return;

                case Yes:
                    // Il ne reste plus que le texte "oui" comme valeur, on le renvoie
                    spellResist.Resist = SpecialBoolean.Yes;
                    return;
            }

            // On va tenter de déchiffrer la valeur spécifique de RM indiquée
            var match = Regex.Match(value, RMPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);

            if (!match.Success)
            {
                // On a pas pu détecter le type de RM, on renvoie spécial
                spellResist.Resist = SpecialBoolean.Special;
                spellResist.Text = MarkupUtil.RemoveMarkup(originalValue).Trim();
                return;
            }

            switch (match.Groups["val"].Value.ToLowerInvariant())
            {
                case Yes:
                    spellResist.Resist = SpecialBoolean.Yes;
                    break;

                case No:
                    spellResist.Resist = SpecialBoolean.No;
                    break;
            }

            foreach (var capture in match.Groups["word"].Captures.Cast<Capture>())
            {
                switch (capture.Value.ToLowerInvariant())
                {
                    case "voir description":
                    case "voir texte":
                    case "spécial":
                        spellResist.Text = value;
                        break;

                    case "inoffensif":
                        spellResist.Harmless = true;
                        break;

                    case "objet":
                    case "[[présentation des sorts#jetsdesauvegarde|objet]]":
                        spellResist.Objects = true;
                        break;

                    default:
                        // ??
                        throw new ParseException(string.Format("Détail de RM non reconnu : {0}", capture.Value));
                }
            }
        }