public void Create_CreatesFlexiSectionBlockAndUpdatesOpenFlexiSectionBlocks() { // Arrange var dummyLine = new StringSlice("dummyLine", 4, 8); const int dummyColumn = 4; const string dummyBlockName = "dummyBlockName"; const string dummyResolvedBlockName = "dummyResolvedBlockName"; const SectioningContentElement dummyElement = SectioningContentElement.Aside; const string dummyLinkIcon = "dummyLinkIcon"; const FlexiSectionBlockRenderingMode dummyRenderingMode = FlexiSectionBlockRenderingMode.Classic; const int dummyLevel = 5; var dummyAttributes = new ReadOnlyDictionary <string, string>(new Dictionary <string, string>()); Mock <IFlexiSectionBlockOptions> dummyFlexiSectionBlockOptions = _mockRepository.Create <IFlexiSectionBlockOptions>(); dummyFlexiSectionBlockOptions.Setup(f => f.BlockName).Returns(dummyBlockName); dummyFlexiSectionBlockOptions.Setup(f => f.Element).Returns(dummyElement); dummyFlexiSectionBlockOptions.Setup(f => f.RenderingMode).Returns(dummyRenderingMode); dummyFlexiSectionBlockOptions.Setup(f => f.LinkIcon).Returns(dummyLinkIcon); dummyFlexiSectionBlockOptions.Setup(f => f.Attributes).Returns(dummyAttributes); Mock <BlockParser> dummyBlockParser = _mockRepository.Create <BlockParser>(); BlockProcessor dummyBlockProcessor = MarkdigTypesFactory.CreateBlockProcessor(); dummyBlockProcessor.Column = dummyColumn; dummyBlockProcessor.Line = dummyLine; var dummyFlexiSectionHeadingBlock = new FlexiSectionHeadingBlock(null); Mock <IFlexiSectionHeadingBlockFactory> mockFlexiSectionHeadingBlockFactory = _mockRepository.Create <IFlexiSectionHeadingBlockFactory>(); mockFlexiSectionHeadingBlockFactory. Setup(f => f.Create(dummyBlockProcessor, dummyFlexiSectionBlockOptions.Object, dummyBlockParser.Object)). Returns(dummyFlexiSectionHeadingBlock); Mock <IOptionsService <IFlexiSectionBlockOptions, IFlexiSectionBlocksExtensionOptions> > mockOptionsService = _mockRepository.Create <IOptionsService <IFlexiSectionBlockOptions, IFlexiSectionBlocksExtensionOptions> >(); mockOptionsService.Setup(o => o.CreateOptions(dummyBlockProcessor)).Returns((dummyFlexiSectionBlockOptions.Object, null)); Mock <FlexiSectionBlockFactory> mockTestSubject = CreateMockFlexiSectionBlockFactory(mockOptionsService.Object, mockFlexiSectionHeadingBlockFactory.Object); mockTestSubject.CallBase = true; mockTestSubject.Setup(t => t.ValidateLevel(dummyLevel)); mockTestSubject.Setup(t => t.ResolveBlockName(dummyBlockName)).Returns(dummyResolvedBlockName); mockTestSubject.Setup(t => t.ValidateElement(dummyElement)); mockTestSubject.Setup(t => t.ValidateRenderingMode(dummyRenderingMode)); mockTestSubject.Setup(t => t.UpdateOpenFlexiSectionBlocks(dummyBlockProcessor, It.IsAny <FlexiSectionBlock>())); // Act FlexiSectionBlock result = mockTestSubject.Object.Create(dummyLevel, dummyBlockProcessor, dummyBlockParser.Object); // Assert _mockRepository.VerifyAll(); Assert.Equal(dummyResolvedBlockName, result.BlockName); Assert.Equal(dummyElement, result.Element); Assert.Equal(dummyLinkIcon, result.LinkIcon); Assert.Equal(dummyRenderingMode, result.RenderingMode); Assert.Equal(dummyLevel, result.Level); Assert.Same(dummyAttributes, result.Attributes); Assert.Same(dummyBlockParser.Object, result.Parser); Assert.Equal(dummyColumn, result.Column); Assert.Equal(dummyLine.Start, result.Span.Start); Assert.Equal(dummyLine.End, result.Span.End); Assert.Single(result); Assert.Same(dummyFlexiSectionHeadingBlock, result[0]); }
public static IEnumerable <object[]> FlexiSectionBlockOptions_CanBePopulated_Data() { const string dummyBlockName = "dummyBlockName"; const SectioningContentElement dummyElement = SectioningContentElement.Aside; const bool dummyGenerateID = false; const string dummyLinkIcon = "dummyLinkIcon"; const bool dummyReferenceLinkable = false; const FlexiSectionBlockRenderingMode dummyRenderingMode = FlexiSectionBlockRenderingMode.Classic; const string dummyAttribute1 = "dummyAttribute1"; const string dummyAttributeValue1 = "dummyAttributeValue1"; var dummyAttributes1 = new Dictionary <string, string> { { dummyAttribute1, dummyAttributeValue1 } }; const string dummyAttribute2 = "dummyAttribute2"; const string dummyAttributeValue2 = "dummyAttributeValue2"; var dummyAttributes2 = new Dictionary <string, string> { { dummyAttribute2, dummyAttributeValue2 } }; return(new object[][] { // Populating FlexiSectionBlockOptions containing default values new object[] { new SerializableWrapper <FlexiSectionBlockOptions>(new FlexiSectionBlockOptions()), new SerializableWrapper <FlexiSectionBlockOptions>(new FlexiSectionBlockOptions(dummyBlockName, dummyElement, dummyGenerateID, dummyLinkIcon, dummyReferenceLinkable, dummyRenderingMode, dummyAttributes1)), $@"{{ ""{nameof(FlexiSectionBlockOptions.BlockName)}"": ""{dummyBlockName}"", ""{nameof(FlexiSectionBlockOptions.Element)}"": ""{dummyElement}"", ""{nameof(FlexiSectionBlockOptions.GenerateID)}"": ""{dummyGenerateID}"", ""{nameof(FlexiSectionBlockOptions.LinkIcon)}"": ""{dummyLinkIcon}"", ""{nameof(FlexiSectionBlockOptions.ReferenceLinkable)}"": ""{dummyReferenceLinkable}"", ""{nameof(FlexiSectionBlockOptions.RenderingMode)}"": ""{dummyRenderingMode}"", ""{nameof(FlexiSectionBlockOptions.Attributes)}"": {{ ""{dummyAttribute1}"": ""{dummyAttributeValue1}"" }} }}" }, // Populating FlexiSectionBlockOptions with an existing attributes collection (should be replaced instead of appended to) new object[] { new SerializableWrapper <FlexiSectionBlockOptions>(new FlexiSectionBlockOptions(attributes: dummyAttributes1)), new SerializableWrapper <FlexiSectionBlockOptions>(new FlexiSectionBlockOptions(attributes: dummyAttributes2)), $@"{{ ""{nameof(FlexiSectionBlockOptions.Attributes)}"": {{ ""{dummyAttribute2}"": ""{dummyAttributeValue2}"" }} }}" } }); }
private static FlexiSectionBlock CreateFlexiSectionBlock(string blockName = default, SectioningContentElement element = default, string linkIcon = default, FlexiSectionBlockRenderingMode renderingMode = default, int level = default, ReadOnlyDictionary <string, string> attributes = default, BlockParser blockParser = default) { return(new FlexiSectionBlock(blockName, element, linkIcon, renderingMode, level, attributes, blockParser)); }
internal virtual void ValidateElement(SectioningContentElement element) { if (!Enum.IsDefined(typeof(SectioningContentElement), element)) { throw new OptionsException(nameof(IFlexiSectionBlockOptions.Element), string.Format(Strings.OptionsException_Shared_ValueMustBeAValidEnumValue, element, nameof(SectioningContentElement))); } }
/// <summary> /// Creates a <see cref="FlexiSectionBlock"/>. /// </summary> /// <param name="blockName">The <see cref="FlexiSectionBlock"/>'s BEM block name.</param> /// <param name="element">The <see cref="FlexiSectionBlock"/> root element type.</param> /// <param name="linkIcon">The <see cref="FlexiSectionBlock"/>'s link icon as an HTML fragment.</param> /// <param name="renderingMode">The <see cref="FlexiSectionBlock"/>'s rendering mode.</param> /// <param name="level">The <see cref="FlexiSectionBlock"/>'s level.</param> /// <param name="attributes">The HTML attributes for the <see cref="FlexiSectionBlock"/>'s root element.</param> /// <param name="blockParser">The <see cref="BlockParser"/> parsing the <see cref="FlexiSectionBlock"/>.</param> public FlexiSectionBlock(string blockName, SectioningContentElement element, string linkIcon, FlexiSectionBlockRenderingMode renderingMode, int level, ReadOnlyDictionary <string, string> attributes, BlockParser blockParser) : base(blockParser) { BlockName = blockName; Element = element; LinkIcon = linkIcon; RenderingMode = renderingMode; Level = level; Attributes = attributes; }
public void ValidateElement_ThrowsOptionsExceptionIfElementIsInvalid() { // Arrange FlexiSectionBlockFactory testSubject = CreateFlexiSectionBlockFactory(); const SectioningContentElement dummyElement = (SectioningContentElement)9; // Act and assert OptionsException result = Assert.Throws <OptionsException>(() => testSubject.ValidateElement(dummyElement)); Assert.Equal(string.Format(Strings.OptionsException_OptionsException_InvalidOption, nameof(IFlexiSectionBlockOptions.Element), string.Format(Strings.OptionsException_Shared_ValueMustBeAValidEnumValue, dummyElement, nameof(SectioningContentElement))), result.Message); }
/// <summary> /// Creates a <see cref="FlexiSectionBlockOptions"/>. /// </summary> /// <param name="blockName"> /// <para>The <see cref="FlexiSectionBlock"/>'s <a href="https://en.bem.info/methodology/naming-convention/#block-name">BEM block name</a>.</para> /// <para>In compliance with <a href="https://en.bem.info">BEM methodology</a>, this value is the <see cref="FlexiSectionBlock"/>'s root element's class as well as the prefix for all other classes in the block.</para> /// <para>This value should contain only valid <a href="https://www.w3.org/TR/CSS21/syndata.html#characters">CSS class characters</a>.</para> /// <para>If this value is <c>null</c>, whitespace or an empty string, the <see cref="FlexiSectionBlock"/>'s block name is "flexi-section".</para> /// <para>Defaults to "flexi-section".</para> /// </param> /// <param name="element"> /// <para>The <see cref="FlexiSectionBlock"/>'s root element's type.</para> /// <para>The element must be a <a href="https://html.spec.whatwg.org/#sectioning-content">sectioning content</a> element.</para> /// <para>Defaults to <see cref="SectioningContentElement.Section"/>.</para> /// </param> /// <param name="generateID"> /// <para>The value specifying whether to generate an ID for the <see cref="FlexiSectionBlock"/>.</para> /// <para>The generated ID is assigned to the <see cref="FlexiSectionBlock"/>'s root element.</para> /// <para>The generated ID is the <see cref="FlexiSectionBlock"/>'s heading content in kebab-case (lowercase words joined by dashes). /// For example, if the heading content is "Foo Bar Baz", the generated ID is "foo-bar-baz".</para> /// <para>If the generated ID is a duplicate of another <see cref="FlexiSectionBlock"/>'s ID, "-<duplicate index>" is appended. /// For example, the second <see cref="FlexiSectionBlock"/> with heading content "Foo Bar Baz" will have ID "foo-bar-baz-1".</para> /// <para>The generated ID precedence over any ID specified in <paramref name="attributes"/>.</para> /// <para>Defaults to true.</para> /// </param> /// <param name="linkIcon"> /// <para>The <see cref="FlexiSectionBlock"/>'s link icon as an HTML fragment.</para> /// <para>A class attribute with value "<<paramref name="blockName"/>>__link-icon" is added to this fragment's first start tag.</para> /// <para>If this value is <c>null</c>, whitespace or an empty string, no link icon is rendered.</para> /// <para>Defaults to the <a href="https://material.io/tools/icons/?icon=link&style=baseline">Material Design link icon</a>.</para> /// </param> /// <param name="referenceLinkable"> /// <para>The value specifying whether the <see cref="FlexiSectionBlock"/> is <a href="https://spec.commonmark.org/0.28/#reference-link">reference-linkable</a>.</para> /// <para>If this value and <paramref name="generateID"/> are both true, the <see cref="FlexiSectionBlock"/> is reference-linkable. /// Otherwise, it isn't.</para> /// <para>If a <see cref="FlexiSectionBlock"/> is reference-linkable, its <a href="https://spec.commonmark.org/0.28/#link-label">link label</a> content /// is its heading content. For example, "## Foo Bar Baz" can be linked to using "[Foo Bar Baz]".</para> /// <para>If a <see cref="FlexiSectionBlock"/>'s ID has "-<duplicate index>" appended (see <paramref name="generateID"/>), /// you can link to it using "<heading content> <duplicate index>". /// For example, the second "## Foo Bar baz" can be linked to using "[Foo Bar Baz 1]".</para> /// <para>Defaults to true.</para> /// </param> /// <param name="renderingMode"> /// <para>The <see cref="FlexiSectionBlock"/>'s rendering mode.</para> /// <para>Defaults to <see cref="FlexiSectionBlockRenderingMode.Standard"/>.</para> /// </param> /// <param name="attributes"> /// <para>The HTML attributes for the <see cref="FlexiSectionBlock"/>'s root element.</para> /// <para>Attribute names must be lowercase.</para> /// <para>If classes are specified, they are appended to default classes. This facilitates <a href="https://en.bem.info/methodology/quick-start/#mix">BEM mixes</a>.</para> /// <para>If the <see cref="FlexiSectionBlock"/> has a generated ID, it takes precedence over any ID in this value.</para> /// <para>If this value is <c>null</c>, default classes are still assigned to the root element.</para> /// <para>Defaults to <c>null</c>.</para> /// </param> public FlexiSectionBlockOptions( string blockName = "flexi-section", SectioningContentElement element = SectioningContentElement.Section, bool generateID = true, string linkIcon = MaterialDesignIcons.MATERIAL_DESIGN_LINK, bool referenceLinkable = true, FlexiSectionBlockRenderingMode renderingMode = FlexiSectionBlockRenderingMode.Standard, IDictionary <string, string> attributes = default) : base(blockName, attributes) { Element = element; GenerateID = generateID; LinkIcon = linkIcon; ReferenceLinkable = referenceLinkable; RenderingMode = renderingMode; }
/// <inheritdoc /> public FlexiSectionBlock Create(int level, BlockProcessor blockProcessor, BlockParser blockParser) { (IFlexiSectionBlockOptions flexiSectionBlockOptions, IFlexiSectionBlocksExtensionOptions _) = _optionsService.CreateOptions(blockProcessor); // Level ValidateLevel(level); // Block name string blockName = ResolveBlockName(flexiSectionBlockOptions.BlockName); // Element SectioningContentElement element = flexiSectionBlockOptions.Element; ValidateElement(element); // Rendering mode FlexiSectionBlockRenderingMode renderingMode = flexiSectionBlockOptions.RenderingMode; ValidateRenderingMode(renderingMode); // Create FlexiSectionBlock var flexiSectionBlock = new FlexiSectionBlock(blockName, element, flexiSectionBlockOptions.LinkIcon, renderingMode, level, flexiSectionBlockOptions.Attributes, blockParser) { Column = blockProcessor.Column, Span = new SourceSpan(blockProcessor.Start, blockProcessor.Line.End), // TODO span should include children // BlockProcessor will assign Line }; // Create FlexiSectionHeadingBlock FlexiSectionHeadingBlock flexiSectionHeadingBlock = _flexiSectionHeadingBlockFactory.Create(blockProcessor, flexiSectionBlockOptions, blockParser); flexiSectionBlock.Add(flexiSectionHeadingBlock); // Close FlexiSectionBlocks with same or lower levels UpdateOpenFlexiSectionBlocks(blockProcessor, flexiSectionBlock); return(flexiSectionBlock); }
public static IEnumerable <object[]> WriteStandard_RendersStandardFlexiSectionBlock_Data() { const string dummyBlockName = "dummyBlockName"; const string dummyLinkIcon = "<dummyLinkIcon></dummyLinkIcon>"; const string dummyLinkIconWithClass = "<dummyLinkIcon class=\"__link-icon\"></dummyLinkIcon>"; const string dummyAttributeKey1 = "dummyAttributeKey1"; const string dummyAttributeValue1 = "dummyAttributeValue1"; const string dummyAttributeKey2 = "dummyAttributeKey2"; const string dummyAttributeValue2 = "dummyAttributeValue2"; const string dummyClass = "dummyClass"; const string dummyID = "dummyID"; const string dummyGeneratedID = "dummyGeneratedID"; const SectioningContentElement dummyElement = SectioningContentElement.Aside; const int dummyLevel = 2; const string dummyHeadingContent = "dummyHeadingContent"; var dummyHeadingContainerInline = new ContainerInline(); dummyHeadingContainerInline.AppendChild(new LiteralInline(dummyHeadingContent)); FlexiSectionHeadingBlock dummyFlexiSectionHeadingBlockWithContent = CreateFlexiSectionHeadingBlock(); dummyFlexiSectionHeadingBlockWithContent.Inline = dummyHeadingContainerInline; return(new object[][] { // BlockName is assigned as a class of the root element and all default classes are prepended with it new object[] { CreateFlexiSectionBlock(dummyBlockName, level: dummyLevel), CreateFlexiSectionHeadingBlock(), $@"<section class=""{dummyBlockName} {dummyBlockName}_level_2 {dummyBlockName}_no-link-icon""> <header class=""{dummyBlockName}__header""> <h2 class=""{dummyBlockName}__heading""></h2> <button class=""{dummyBlockName}__link-button"" aria-label=""Copy link""> </button> </header> </section> " }, // If LinkIcon is valid HTML, it is rendered with a default class and a _has-link-icon class is rendered new object[] { CreateFlexiSectionBlock(linkIcon: dummyLinkIcon, level: dummyLevel), CreateFlexiSectionHeadingBlock(), $@"<section class="" _level_2 _has-link-icon""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> {dummyLinkIconWithClass} </button> </header> </section> " }, // If LinkIcon is null, whitespace or an empty string, no copy icon is rendered and a _no-link-icon class is rendered (null case verified by other tests) new object[] { CreateFlexiSectionBlock(linkIcon: " ", level: dummyLevel), CreateFlexiSectionHeadingBlock(), @"<section class="" _level_2 _no-link-icon""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </section> " }, new object[] { CreateFlexiSectionBlock(linkIcon: string.Empty, level: dummyLevel), CreateFlexiSectionHeadingBlock(), @"<section class="" _level_2 _no-link-icon""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </section> " }, // If an ID is generated, it is written new object[] { CreateFlexiSectionBlock(level: dummyLevel), CreateFlexiSectionHeadingBlock(generatedID: dummyGeneratedID), $@"<section class="" _level_2 _no-link-icon"" id=""{dummyGeneratedID}""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </section> " }, // If attributes specified, they're written new object[] { CreateFlexiSectionBlock(level: dummyLevel, attributes: new ReadOnlyDictionary <string, string>(new Dictionary <string, string> { { dummyAttributeKey1, dummyAttributeValue1 }, { dummyAttributeKey2, dummyAttributeValue2 } })), CreateFlexiSectionHeadingBlock(), $@"<section class="" _level_2 _no-link-icon"" {dummyAttributeKey1}=""{dummyAttributeValue1}"" {dummyAttributeKey2}=""{dummyAttributeValue2}""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </section> " }, // Class attribute value is appended to default classes new object[] { CreateFlexiSectionBlock(level: dummyLevel, attributes: new ReadOnlyDictionary <string, string>(new Dictionary <string, string> { { "class", dummyClass } })), CreateFlexiSectionHeadingBlock(), $@"<section class="" _level_2 _no-link-icon {dummyClass}""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </section> " }, // Generated ID takes precedence over any ID in attributes new object[] { CreateFlexiSectionBlock(level: dummyLevel, attributes: new ReadOnlyDictionary <string, string>(new Dictionary <string, string> { { "id", dummyID } })), CreateFlexiSectionHeadingBlock(generatedID: dummyGeneratedID), $@"<section class="" _level_2 _no-link-icon"" id=""{dummyGeneratedID}""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </section> " }, // Specified sectioning content element is rendered new object[] { CreateFlexiSectionBlock(element: dummyElement, level: dummyLevel), CreateFlexiSectionHeadingBlock(), $@"<{dummyElement.ToString().ToLower()} class="" _level_2 _no-link-icon""> <header class=""__header""> <h2 class=""__heading""></h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </{dummyElement.ToString().ToLower()}> " }, // Header content is rendered new object[] { CreateFlexiSectionBlock(level: dummyLevel), dummyFlexiSectionHeadingBlockWithContent, $@"<section class="" _level_2 _no-link-icon""> <header class=""__header""> <h2 class=""__heading"">{dummyHeadingContent}</h2> <button class=""__link-button"" aria-label=""Copy link""> </button> </header> </section> " } }); }