in src/lib/Microsoft.Fx.Portability/BreakingChangeParser.cs [60:235]
public static IEnumerable<BreakingChange> FromMarkdown(Stream stream, IEnumerable<string> allowedCategories)
{
var breakingChanges = new List<BreakingChange>();
var state = ParseState.None;
using (var sr = new StreamReader(stream))
{
BreakingChange currentBreak = null;
string currentLine;
while ((currentLine = sr.ReadLine()) != null)
{
currentLine = currentLine.Trim();
// New breaking change
if (currentLine.StartsWith("## ", StringComparison.Ordinal))
{
// Save previous breaking change and reset currentBreak
if (currentBreak != null)
{
CleanAndAddBreak(breakingChanges, currentBreak);
}
currentBreak = new BreakingChange();
// Separate ID and title
var substring = currentLine.Substring("## ".Length).Trim();
var splitTitle = substring.Split(new[] { ':' }, 2);
if (splitTitle.Length == 1)
{
// Breaking changes are keyed on title, not ID, so if ':' is missing, just take the line as a title.
// Note that this will make it impossible to suppress the breaking change, though.
currentBreak.Title = splitTitle[0].Trim();
}
else if (splitTitle.Length == 2)
{
if (int.TryParse(splitTitle[0], out var id))
{
currentBreak.Id = id.ToString(CultureInfo.InvariantCulture);
currentBreak.Title = splitTitle[1].Trim();
}
else
{
currentBreak.Title = substring;
}
}
// Clear state
state = ParseState.None;
}
// Only parse breaking change if we've seen a breaking change header ("## ...")
else if (currentBreak != null)
{
// State changes
if (currentLine.StartsWith("###", StringComparison.Ordinal))
{
switch (currentLine.Substring("###".Length).Trim().ToLowerInvariant())
{
case "scope":
state = ParseState.Scope;
break;
case "version introduced":
case "version broken":
state = ParseState.VersionBroken;
break;
case "version reverted":
case "version fixed":
state = ParseState.VersionFixed;
break;
case "change description":
case "details":
state = ParseState.Details;
break;
case "recommended action":
case "suggestion":
state = ParseState.Suggestion;
break;
case "affected apis":
case "applicableapis":
state = ParseState.AffectedAPIs;
break;
case "original bug":
case "buglink":
case "bug":
state = ParseState.OriginalBug;
break;
case "notes":
state = ParseState.Notes;
break;
case "source analyzer status":
state = ParseState.SourceAnalyzerStatus;
break;
case "category":
case "categories":
state = ParseState.Categories;
break;
default:
ParseNonStateChange(currentBreak, state, currentLine, allowedCategories);
break;
}
}
// Bool properties
else if (currentLine.StartsWith("- [ ]", StringComparison.Ordinal) ||
currentLine.StartsWith("- [x]", StringComparison.OrdinalIgnoreCase))
{
bool isChecked = currentLine.StartsWith("- [x]", StringComparison.OrdinalIgnoreCase);
switch (currentLine.Substring("- [x]".Length).Trim().ToLowerInvariant())
{
case "quirked":
case "isquirked":
currentBreak.IsQuirked = isChecked;
state = ParseState.None;
break;
case "build-time break":
case "isbuildtime":
currentBreak.IsBuildTime = isChecked;
state = ParseState.None;
break;
default:
ParseNonStateChange(currentBreak, state, currentLine, allowedCategories);
break;
}
}
// More info link
else if (currentLine.StartsWith("[More information]", StringComparison.OrdinalIgnoreCase))
{
currentBreak.Link = currentLine.Substring("[More information]".Length)
.Trim(' ', '(', ')', '[', ']', '\t', '\n', '\r') // Remove markdown link enclosures
.Replace("\\(", "(").Replace("\\)", ")"); // Unescape parens in link
state = ParseState.None;
}
// Comments.
else if (currentLine.StartsWith("<!--", StringComparison.Ordinal))
{
if (state == ParseState.Suggestion)
{
// suggestions may contain comments; these are part of the suggestion
ParseNonStateChange(currentBreak, state, currentLine, allowedCategories);
}
else if (currentLine.EndsWith("-->", StringComparison.Ordinal))
{
// change IDs are in single-line comments, e.g. <!-- breaking change id: 144 -->
string id = currentLine.Split()
.FirstOrDefault(token => int.TryParse(token, out _));
currentBreak.Id = currentBreak.Id ?? id;
state = ParseState.None;
}
else
{
// this line begins a multi-line comment not part of a suggestion
state = ParseState.Comment;
}
}
// Otherwise, process according to our current state
else
{
ParseNonStateChange(currentBreak, state, currentLine, allowedCategories);
}
}
}
// Add the final break from the file
if (currentBreak != null)
{
CleanAndAddBreak(breakingChanges, currentBreak);
}
}
return breakingChanges;
}