FIT2102ProgrammingParadigms2024
Assignment2:MarkdowntoHTML
DueDate:Friday,18thOctober,11:55pm
Weighting:30%ofyourfinalmarkfortheunit
Interview:SWOTVAC+Week13
Overview:Studentswillworkindependentlytocreateaparserforasubsetofthe
Markdownspecificationusingfunctionalprogrammingtechniques.Programswillbe
implementedinHaskell.Thegoalistodemonstrateagoodunderstandingof
functionalprogrammingtechniquesasexploredthroughouttheunit,including
writtendocumentationofthedesigndecisionsandfeatures.
SubmissionInstructions
Submitazippedfilenamed
named
●Itmustcontainallthecodethatwillbemarkedincludingthereportandall
codefiles
●Youalsoneedtoincludeareportdescribingyourdesigndecisions.Thereport
mustbenamed
●NoadditionalHaskelllibrariesshouldbeused
○Youmayuseadditionallibrariesonlyfortestingpurposes.
●Beforezipping,runstackclean--full(toensureasmallbundle)
●Donotsubmitnode_modulesorthe.gitfolder
●Makesurethecodeyousubmitexecutesproperly.
Themarkingprocesswilllooksomethinglikethis:
1.Extract
2.Copythesubmissionfoldercontentsintotheassignmentcodebundle
submissionfolder
3.Executestackbuild,stacktest(forautomatedtesting)andstack
execmain/npmrundevforfrontend
Pleaseensurethatyoutestthisprocessbeforesubmitting.Anyissuesduringthis
processwillmakeyourmarkerunhappyandmayresultinadeductioninmarks.
Latesubmissionswillbepenalisedat5%percalendarday,roundedup.Late
submissionsmorethansevendayswillreceivezeromarksandnofeedback.
TableofContents
Assignment2:MarkdowntoHTML1
SubmissionInstructions1
TableofContents2
Introduction3
Goals/LearningOutcomes3
Scopeofassignment3
Exercises(24marks)4
PartA:(12marks):ParsingMarkdown5
Aside-TextModifiers(2marks)5
Images(0.5marks)6
FootnoteReferences(0.5marks)6
FreeText(1mark)6
Headings(1mark)6
Blockquotes(1mark)7
Code(1mark)8
OrderedLists(2marks)8
Tables(3marks)8
PartB:(6marks):HTMLConversion9
TextModifiers(1mark)9
Images(0.5marks)10
FootnoteReferences(0.5marks)10
FreeText(0.5marks)10
Headings(0.5marks)10
Blockquotes(0.5marks)10
Code(0.5marks)11
OrderedLists(1marks)11
Tables(1mark)11
PartC(6marks):Addingextrafunctionalitytothewebpage13
PartD(upto6bonusmarks):Extension14
Report(2marks)15
CodeQuality(4marks)16
Markingbreakdown17
Correctness17
MinimumRequirements:18
Changelog18
Introduction
Inthisassignment,wewilluseHaskelltodevelopatranspilerthatconvertsMarkdown
stringsintoHyperTextMarkupLanguage(HTML).ThistaskinvolvesparsingMarkdown
syntaxandgeneratingcorrespondingHTMLoutput.Awebpageisprovided,inwhich
MarkdownwillbesentthroughanHTML-basedwebsocketconnectiontoaHaskell
backendserver,theHaskellserverwillneedtoconvertthisMarkdownintothe
correspondingHTMLandreturnitbacktothewebsite.Askeletoncodewasprovided
whichwillhandlethebasiccommunicationbetweenthewebpageandyourassignment
code.
Youareencouragedtoutilisematerialscoveredinpreviousweeks,includingsolutions
fortutorialquestions,toaidinthedevelopmentofyourtranspiler.Youmustreference
orciteideasandcodeconstructsobtainedfromexternalsources,aswellas
anythingelseyoumightfindinyourindependentresearch,forthisassignment.
TheassignmentissplitupintoPartA(parsing),PartB(prettyprinting)andPartC
(extras).However,wedorecommendcompletingPartA/PartBintandem.
ThelanguageyouwillparsewillbebasedontheMarkdownspecification,howeverwith
additionalrestrictionstoreduceambiguity.Itisimportantthatyoureadtherequirements
ofeachexercisecarefullytoavoidunnecessarywork.
Goals/LearningOutcomes
Thepurposeofthisassignmentistohighlightandapplytheskillsyouhavelearnedtoa
practicalexercise(parsing):
●Usefunctionalprogrammingandparsingeffectively
●Understandandbeabletousekeyfunctionalprogrammingprinciples(higher
orderfunctions,purefunctions,immutabledatastructures,abstractions)
●ApplyHaskellandFPtechniquestoparsenon-trivialMarkdowntext
Scopeofassignment
Youareonlyrequiredtoparseanexpressionintothenecessarydatatypesandconvert
theresulttoanHTMLstringsuchthatitcanberenderedusinganexistinginterpreter.
YouwillnotberequiredtorendertheMarkdownorHTMLstrings.
Exercises(24marks)
Theseexercisesprovideastructuredapproachforcreatingthebeginningsofa
transpiler.
●PartA(12marks):ParsingMarkdownstrings
●PartB(6marks):ConversionbetweenMarkdownandHTML
●PartC(6marks):Addingextrafunctionalitytothewebpage.
●(Extension)PartDPartE:extensionsforbonusmarks!
Youmustparsetheinputintoanintermediaryrepresentation(ADT)suchasan
AbstractSyntaxTreetoreceivemarks.Thiswillalloweasyconversionbetween
yourADTandHTML.
YoumustaddderivingShowtoyourADTandallcustomtypesyourADT
contains.(NotethattheskeletoncodealreadyhasderivingShowontheADTtype
foryou,whichyoumustnotremove.)YoumustnotoverridethisdefaultShow
instanceasthiswillhelpustestyourcode.
YourAssignment.hsfilemustexportthefollowingfunctions:
●markdownParser::ParserADT
●convertADTHTML::ADT->String
ExampleScripts
Foreachoftheseexercises,therewillbeaseriesofprovidedMarkdownfiles.By
runningstacktest,itwilltrytoparsetheMarkdownandsavetheoutputtoafolder.
ThiswillgenerateHTMLwhichyoucanmanuallyviewforcorrectnessinabrowser.
Duringmarking,wewillberunningyourtranspileronmorecomplexexamplesthanthe
providedexamplescripts,therefore,itisimportantyoudeviseyourowntestcasesto
ensureyourparserisvalidonmorecomplexMarkdown.Itwillalsoaimtoproduceagit
diff,whichisthedifferencebetweenyouroutputandtheexpectedoutput.However,this
requiresinstallingthegitcommandlinetool.So,ensurethatitisinstalled.
Furthermore,themorerecommendedwaytotestyourcodewillbetousenpmrun
devincombinationwithstackrunmaincanbeusedtorunthewebpagewithalive
editor,runningyourcodeinreal-time.
PartA:(12marks):ParsingMarkdown
Thefirstpartofthistask,requiresyoutoparseamarkdownstringintoanAlgebraic
DataType(ADT).ThisrequiresyoutodefineyourownAlgebraicDataTypeanddefine
aseriesoffunctionsthatparseeverythingintherequirements.Considerthatyouwill
needtoconverttheresulttoHTMLandtherefore,yourADTshouldhaveenough
informationtoassistyouinconvertingtoHTML.
Aside-TextModifiers(2marks)
Therearesixdifferentmodifiersforinlinetext,whichcanchangethewaya
markdownstringwillberendered.Youdonothavetoworryaboutanyescape
characters.Alltextmodifierswillneedtobestrictlynon-empty.
●ItalicText:Specifiedbyasingleunderscorecharacter,_.Forexample,
_italics_
●BoldText:Specifiedbyasetoftwoasterisks,**,aroundaword.Forexample,
**bold**
●Strikethrough:Specifiedbytwotildecharacters,~~,aroundaword.Forexample,
~~strikethrough~~
●Link:Userscanincludealinktoanexternalpageusing[linktext](URL).
Forexample,[clickhere](www.google.com).Youdonotneedto
considerlinksinsidelinks.
●InlineCode:Userscanincludecodeinthemiddleofsentences,usingabacktick
character,`.Forexample,thereis`code`here
●Footnotes:Userscanindicateafootnotewith[^ℤ
+
],whereℤ
+
={1,2,3,...},i.e.,
anypositiveinteger.Forexample,[^1],[^2]andsoforth.Notethatyoudo
notneedtovalidateanysortoforderingonthesenumbers,e.g.,themarkdown
mayonlycontainonefootnote[^10].Youalsodonotneedtovalidatethatthe
footnotecomeswithanappropriatereference(seeFootnoteReferences).
○Notethattheremustnotbeanywhitespaceinsidethe[and].For
example,[^1],[^2],and[^3]areallnotvalidfootnotes.
Youdonotneedtoconsidertextwithnestedmodifiers,suchas**_boldand
italics_**.
Unlessspecifiedotherwise,thetextinsidethemodifierscanincludeanyamountof
whitespace(excludingnewlines).Forexample,_italics_,**bold**,~~
strikethrough~~,`inlinecode`,and[linktext](example.com),and
[linktext](example.com)areallvalid.
Images(0.5marks)
Animageisspecifiedwiththreeparts:
1.TheAltTextisthealternativetextfortheimage,whichisdisplayedifthe
imagefailstoloadorforaccessibilitypurposes.
2.TheURListheURLorpathtotheimagefile.ThiscanbeawebURLoralocalfile
path.TheURLcannotcontainanywhitespace.
3.TheCaptionTextisthecaptionfortheimage.

Thealternativetext,captiontext,andURLshouldnotconsiderthetextmodifiers.
Theremustbeatleastonenon-newlinewhitespacecharacterbetweentheURLand
thecaptiontext.Forexample,isnotavalid
image.
Theremustnotbeanyspacesafterthe!andbeforethe[.
FootnoteReferences(0.5marks)
Similarlytofootnotes,footnotereferencesconsistof[^ℤ
+
],whereℤ
+
={1,2,3,...},i.e.,
anypositiveinteger,followedbyacolon(:),andthensometext.Notethatthistextwill
notincludethetextmodifiers.Leadingwhitespacebeforethetextshouldbeignored.
[^1]:Myreference.
[^2]:Anotherreference.
[^3]:The2spacesafterthecolonshouldbeignored
FreeText(1mark)
Therecanbeanyamountoftextwhichdoesnotfollowanyofthefollowingothertypes.
Thistextmaycontainthemodifiers.Forexample:
Hereissome**markdown**
Morelineshere
Text
Headings(1mark)
Markdownheadingsaredenotedbyoneormorehashsymbols(#),followedbyatleast
onewhitespacecharacter(excludingnewlines).Therecanbeupto6#’s,producinga
headinguptolevel6.
#Heading1
##Heading2
###Heading3
####Heading4
#####Heading5
######Heading6
Notethatbecauseatleastonenon-newlinewhitespacecharacterisrequired,thisisnot
avalidheading:#Heading1
Alternatively,Heading1andHeading2canbespecifiedwithanalternativesyntax
(shownbelow).Onthelinebelowthetext,addatleast2equalssign(=)charactersfor
headinglevel1oratleast2dash(-)charactersforheadinglevel2.Thereisno
alternativesyntaxforanyotherheadinglevels.
AlternativeHeading1
======
Headinglevel2
---------------
Importantly,headingsmayincludeanyofthepreviouslymentionedtextmodifiers,for
example,aheadingcanbebolded,bysurroundingitwithadoubleasterisk.
#**BoldedHeading1**
Blockquotes(1mark)
TocreateablockquoteinMarkdown,youusethegreaterthansymbol(>)atthe
beginningofalinefollowedbythetextyouwanttoquote.Youcanalsoincludemultiple
linesoftextwithinthesameblockquotebystartingeachconsecutivelinewiththe
greaterthansymbol(>).Leadingwhitespaceafterthegreaterthansymbol(>)and
beforethetextshouldbeignored.Thetextinsidetheblockquotemayhavetext
modifiers.Youdonotneedtoconsidernestedblockquotes.Forexample:
>Thisisablockquote.
>Itcan**span**multiplelines.
Code(1mark)
AcodeblockinMarkdownstartswiththreebackticks(```)onalinebythemselves,
followedbyanoptionallanguageidentifier.Thecodeblockendswithanotherthree
backticksonalinebythemselves.Thecodeblockshouldnotconsiderthetext
modifiers.Anexamplecodeblockis:
```haskell
main::IO()
main=do
putStrLn"Nevergonnagiveyouup"
putStrLn"Nevergonnaletyoudown"
putStrLn"Nevergonnarunaroundanddesertyou"
```
OrderedLists(2marks)
Anorderedlistconsistsofatleastoneorderedlistitemseparatedbyexactly1newline
character.Anorderedlistitemstartswithapositivenumber,a.(fullstop)character,
andatleastonewhitespacecharacter(excludingnewlines).Anorderedlistmuststart
withthenumber1,andanynumberafterthatcanappear.Youdonothavetoconsider
anyothernumberingsystemoranunorderedlist.
Orderedlistsmaycontainsublists,wheretherewillbeexactly4spacesbeforeeach
orderedlistitem.Eachsublistmustalsostartwiththenumber1.Similartoprevious
sections,listitemsmayalsocontaintextmodifiers.
1.Item1
1.SubItem1
2.SubItem2
3.SubItem3
2.**BoldedItem2**
6.Item3
7.Item4
Youdonothavetohandleunorderedlists.
Tables(3marks)
TocreateatableinMarkdown,youusepipes(|)toseparatecolumnsandatleast
threedashes(-)betweeneachcolumntoseparatetheheaderrowfromthecontent
rows.Eachcolumnmaycontainvaryingamountsofdashes.Eachrowiswrittenona
separateline.Thebeginningandendingpipes(|)arecompulsory.Eachrowmusthave
thesameamountofcolumns.Eachcellmayalsocontaintextwiththetextmodifiers.
Leadingandtrailingwhitespacebeforeandafterthetextineachcellshouldbeignored.
|Tables|Are|Cool|
|-------------|-------------|-----|
|here|is|data|
|here|is|data|
|here|isalso|**boldeddata**|
PartB:(6marks):HTMLConversion
ThesecondpartofthistaskrequiresyoutoconvertyourADTintoaHTML
representation.TheresultingHTMLfilemustbeformattedsuchthatitisindentedwith4
spacesatthecorrectleveltoreflectthetreestructureofHTML,ensuringthattheHTML
isvalidandcorrectlyrenderstheprovidedmarkdown.Youdonotneedtoindentthetext
modifiers,butothernestedobjectsshouldbeindententedcorrectly.
AllHTMLgeneratedmustbeaself-containedwebpage,i.e.,includingthefollowing
information,placingallgeneratedHTMLwithinthe
tags.GENERATEDCONTENTGOESHERE
AsareferencefortheconversionbetweenmarkdownandHTML,herewillbelistedthe
conversionofalloftheexamplesfromabove.
TextModifiers(1mark)
●Italics:italics
●Bold:bold
●Strikethrough:strikethrough
●Link:
●InlineCode:code
●Footnotes:
importantthatyoufollowthisconventionprecisely,where1isthenumber
specifiedwiththefootnote,toensurethefootnoteswork.
Images(0.5marks)
Theimagemustbeinanimagetag,withtheappropriateattributesfilled.
FootnoteReferences(0.5marks)
Afootnotereferencemustbeencasedina
tag,andhavetheappropriately
numberedid.
spaces.
FreeText(0.5marks)
Everylineoffreetextmustbeencasedin
tags.Youdonotneedtoconsiderhowto
handlenewlines.
Hereissomemarkdown
Morelineshere
Text
Headings(0.5marks)
Where,thenumberaftertheh,containstheleveloftheheading,forexample,in
headinglevel1:
Heading1
Blockquotes(0.5marks)
Eachblockquotemustbeencasedby
,whileeachlinewithintheblockquotemustbeencasedwitha
tag.
Thisisablockquote.
Itcanspanmultiplelines.
Code(0.5marks)
Thecodeblockmustbeencasedinboththe
andthetags,andthelanguage(e.g.,haskell)mustbeincludedwithintheclassattribute,prefixedby
language-.Thenewlinesandcodeindentationmustremain.
main::IO() main=do
putStrLn"Nevergonnagiveyouup"
putStrLn"Nevergonnaletyoudown"
putStrLn"Nevergonnarunaroundanddesertyou"
OrderedLists(1marks)
Orderedlistsmustbeginandendwiththe
tag,andeachlistitemmustbeginand
endwiththeopening/closing
- tag.
- Item1
- SubItem1
- SubItem2
- SubItem3
- BoldedItem2
- Item3
- Item4
Tables(1mark)
TheHTMLconventionforrepresentingtablesinvolvesusingthe
,
, ,and elements. representstheentiretable,
representsa rowwithinthetable,
representsaheadercellwithinatablerow,usedforthe headerrow,and
representsadatacellwithinatablerow,usedforthecontent rows.
Tables Are Cool here is data here is data here isalso boldeddata PartC(6marks):Addingextrafunctionalitytothewebpage
Thistaskinvolveschangingthewebpagetoincludeextracapabilitiesallowingamore
feature-fullUI.Youwillnotbemarkedonthelayout,oreaseofuseoffeatures,aslong
astheyareclearlyvisibletoyourmarker,e.g.,abuttonshouldbeclearlyvisibleonthe
screen.ThistaskwillinvolvesomelightadditionstoboththeHTMLpageand
TypeScriptcode.Thiswilllikelyinvolvecreatinganobservablestreamforthedata,
mergingitintothesubscriptionstream,andsendingtheinformationtotheHaskell
backend.ThecommunicatedinformationbetweentheHaskellbackendandthe
webpagewillneedtobeupdatedtoincludeadditionalinformationthattheuserwants
theenginetoachieve.
●Abuttonmustbeaddedtothewebpageforsaving,wheretheconvertedHTML
issavedusingHaskell.Theuserdoesnotneedtobepromptedforafilename,
andtheHTMLshouldbesavedaccordingtothecurrenttime,formattedinISO
8601formatforthecurrentdateandtime:YYYY-MM-DDTHH:MM:SS.The
functiongetTimeisprovidedwhichwillprovideyouthistimeinanIOString
format.
●Aseparateinputbox,toallowtheusertochangethetitleofthepage,insteadof
thedefaultConvertedHTML.
PartD(upto6bonusmarks):Extension
Implementanythingthatisinteresting,impressive,orotherwise“showsoff”your
understandingofHaskell,FunctionalProgramming,and/orParsing.
Toachievethemaximumamountofbonusmarks,thefeatureshouldbesimilarin
complexitytoPartC(6marks):
Thebonusmarksonlyapplytothisassignment,andthefinalmarkforthisassignment
iscappedat30marks(100%).Thismeansyoucannotscoremorethan30marksor
100%.
Somesuggestionsforextensionsofvaryingcomplexityanddifficulty:
●Markdownvalidation
○E.g.,enforcealltablecolumnshavethesamewidth
●CorrectBNFfortheMarkdownyouareparsinginreport(worth2marks)
○Foranypartoftheparserwhichisnotcontext-free,youmaysimplifythe
parsingrulestobecontext-free.
●Furtherextensionstothewebpageforextrafeatures,usingRxJS
●Parsenestedtextmodifiers,suchas**_boldanditalics_**and[click
**here**](https://example.com)
●Parsefurtherpartsofthemarkdownspecificationwhichmakeuseofinteresting
parsers,whichyouhavenotusedinotherpartsoftheassignment.
●Comprehensivetestcasesovertheparserandprettyprinting
○Warning:Itissuperhardtobecomprehensive,stayawayunlessyoulove
testing.
(Choosingoneofthesimplersuggestionstoimplementmaynotreceivethemaximum
availablemarks).
Report(2marks)
YouarerequiredtoprovideareportinPDFformatofmax.600words(markerswillnot
markbeyondthiswordlimit).Descriptionsofextensionscanuseupto200wordsper
extensionfeature.
Makesuretosummarisetheintentionofthecode,andhighlighttheinterestingparts
anddifficultiesyouencountered.Focusonthe"why"notthe"how".
Additionally,justpostingscreenshotsofcodeisheavilydiscouraged,unlessit
containssomethingofparticularimportance.Remember,markerswillbelookingatyour
codealongsideyourreport,sowedonotneedtoseeyourcodetwice.
Importantly,thisreportmustincludeadescriptionofwhyandhowparsercombinators
helpedyoucompletetheparsing.Insummary,yourreportshouldincludethefollowing
sections:
●Designofthecode(includingdatastructures)
○High-leveldescriptionofapproach
○High-levelstructureofcode
○Codearchitecturechoices
●Parsing
○Usageofparsercombinators
○Choicesmadeincreatingparsersandparsercombinators
○HowparsersandparsercombinatorswereconstructedusingtheFunctor,
Applicative,andMonadtypeclasses
●FunctionalProgramming(focusingonthewhy)
○Smallmodularfunctions
○Composingsmallfunctionstogether
○Declarativestyle(includingpointfreestyle)
●HaskellLanguageFeaturesUsed(focusingonthewhy)
○TypeclassesandCustomTypes
○Higherorderfunctions,fmap,apply,bind
○Functioncomposition
●DescriptionofExtensions(ifapplicable)
○Whatyouintendedtoimplement
○Whatyoudidimplement
○Whatiscool/interesting/complexaboutit
○ThismayincludeusingHaskellfeaturesthatarenotcoveredincourse
content
Thereissomeoverlapbetweenthesections.Youshouldavoidrepeatingdescriptions
orideasinthereport.
CodeQuality(4marks)
Codequalitywillrelatemoretohowunderstandableyourcodeis.Youmusthave
readableandfunctionalcode,commentedwhennecessary.Readablecodemeans
thatyoukeepyourlinesatareasonablelength(<80characters),thatyouprovide
commentsabovenon-trivialfunctions,andthatyoucommentsectionsofyourcode
whosefunctionmaynotbeclear.
Yourfunctionsshouldallbesmallandmodular,buildingupincomplexity,andtaking
advantageofbuilt-infunctionsorself-definedutilityfunctionswhenpossible.Itshould
beeasytoreadandunderstandwhateachpieceofyourcodeisdoing,andwhyitis
useful.Donotreimplementlibraryfunctions,suchasmap,andusetheappropriate
libraryfunctionwhenpossible.
Yourcodeshouldaimtore-usepreviousfunctionsasmuchaspossible,andnotrepeat
workwhenpossible.
CodequalityincludesyourADTandifitiswellstructured,i.e.,doesnothaveabunchof
repeateddatatypesandfollowsalogicalmanner(theJSONexamplefromtheapplied
sessionisagoodexampleofwhatanADTshouldlooklike).
Markingbreakdown
Themainmarkingcriteriaforeachparsingandprettyprintingexerciseconsistsoftwo
parts:correctnessandFPstyle.BothcorrectnessandFPstylewillbeworth50%of
themarksforeachoftheexercises,i.e.,ifyourcodepassesalltests,youwillgetat
leasthalfmarksforExerciseA,andExerciseB.
Youwillbeprovidedwithsomesampleinputandtestsfordeterminingthevalidityofthe
outputtedHTMLfiles.Thesampleinputsprovidedwillnotbeexhaustive,youare
heavilyencouragedtoaddyourown,perhapscoveringedgecases.
Correctness
Wewillberunningaseriesoftestswhichtesteachexercise,anddependingonhow
manyofthetestsyoupass,aproportionofmarkswillbeawarded
FPStyle
FPstylerelatestoifthecodeisdoneinawaythatalignswiththeunitcontentand
functionalprogramming.
Youmustapplyconceptsfromthecourse.Theimportantthinghereisthatyouneedto
usewhatwehavetaughtyoueffectively.Forexample,defininganewtypeandits
Monadinstance,butthenneveractuallyneedingtouseitwillnotgiveyoumarks.Note:
usingbind(>>=)forthesakeofusingtheMonadwhenitisnotneededwillnotcount
as"effectiveusage."
Mostimportantly,codethatdoesnotutiliseHaskell'slanguagefeatures,andthat
attemptstocodeinamoreimperativestyle,willnotbeawardedhighmarks.
MinimumRequirements:
Anestimateofapassinggradewillbeparsinguptoandincludingcodeblocks,butnot
listsortables,wherethedifficultyandthemarksstepup.However,thiswillneedtobe
accompaniedbyhighcodequalityandagoodreport.
Ahighermarkwillrequireparsingofthemoredifficultdatastructures,andmodifications
oftheHTMLpage.
Changelog
●Addnotethattextmodifiersmustbenon-empty
●AddnoteaboutBNFcansimplifyparser,ifandonlyiftheparserisnotcontext
free.
●18Sep:Removetherequirementtoparsenestedtextmodifiersandinstead
makethatanextension
●18Sep:FixissueinscaffoldwherefrontendwouldshowoutputHTMLwitha
leadingandtrailingquote
●20Sep:Changed“AbstractDataType”to“AlgebraicDataType”(underPartA)
●24Sep:ClarifythatURLsinimagesshouldnotconsidertextmodifiers
●25Sep:Clarifythatthereshouldbenospacesafter!andbefore[inimages
![]()
Email:51zuoyejun
@gmail.com
添加客服微信: Fudaojun0228